← Back to build log
Power Hardware

I had to rewrite the whole codebase

Jul 5, 2026 · 7 min read · Phase 1 — Head Electronics

Not the first time, but didn't expect to be rewriting the whole thing as soon as this.

What happened

Can’t believe I’ll say this, but AI trends and rush got to me. Could be the excitement of finally getting a working prototype, or the excitement of finally getting a working prototype, and just wanting to see R2 blink and beep as soon as possible. I’ve ended up vibe coding the first version of the R2 codebase. Aaand, code worked… generation was fast… it was fun.

BUT

I got disconnected from the project. The 3D model, the electronics, everything done by hand, and I was actually proud of the progress I’ve made in a short time. The thing is, everything in this project was, at least somewhat, new to me. I’ve never created a complex 3D model. I’ve never connected a circuit before. Never written a project in GO. I took up everything new so that I’d learn it and could use it in the future. I didn’t go into it expecting to acquire expert level knowledge, but I wanted to learn. And I did.
Everything but GO…

It was vibe coded. It was messy. I mean, the codebase organization was okay. But looking as an outsider, it seemed like a mess that works. And the worst part… I didn’t know how what worked. Looking at the pin_output.go file (just an example, the feeling propagated through the whole codebase), which dealt with sound conversion to PWM signals, I couldn’t for the life of me figure out why it was written that way.

The code wasn’t badly written, maybe a few variables could’ve used a better name, but it was overall okay.

The problem was me.

I didn’t understand it. The code was foreign, the logic something I never needed to understand until now. So yeah… I was lost.

Let’s look at pin_output.go

const (
	PinSampleRate = 44100
	pwmCycleLen   = 1024
	audioPin      = 18
)

type PinOutput struct {
	pin      rpio.Pin
	cycleLen uint32
}

func NewPinOutput() (*PinOutput, error) {
	if err := rpio.Open(); err != nil {
		return nil, err
	}
	pin := rpio.Pin(audioPin)
	pin.Mode(rpio.Pwm)
	pin.Freq(PinSampleRate * int(pwmCycleLen))
	pin.DutyCycle(pwmCycleLen/2, pwmCycleLen)
	return &PinOutput{pin: pin, cycleLen: pwmCycleLen}, nil
}

func (o *PinOutput) PlaySync(streamer beep.Streamer) {
	buf := make([][2]float64, 256)
	samplePeriod := time.Second / time.Duration(PinSampleRate)

	for {
		n, ok := streamer.Stream(buf)
		for i := 0; i < n; i++ {
			sample := (buf[i][0] + buf[i][1]) * 0.5
			duty := uint32((sample+1.0)*0.5*float64(o.cycleLen) + 0.5)
			if duty > o.cycleLen {
				duty = o.cycleLen
			}
			o.pin.DutyCycle(duty, o.cycleLen)
			time.Sleep(samplePeriod)
		}
		if !ok {
			break
		}
	}
	o.pin.DutyCycle(o.cycleLen/2, o.cycleLen)
}

func (o *PinOutput) Close() {
	o.pin.DutyCycle(o.cycleLen/2, o.cycleLen)
	rpio.Close()
}

What the hell is SampleRate? What is a PWM cycle? Why the fluff is this conversion:

sample := (buf[i][0] + buf[i][1]) * 0.5
duty := uint32((sample+1.0)*0.5*float64(o.cycleLen) + 0.5)

Even when Claude explained it to me, I still felt a distance from it. That doesn’t usually happen. I’ve worked on a bunch of projects with other people and am used to jumping into the unknown.
What I’m not used to is looking at my own project and not being able to write a single line of code.

So I decided.

I’ll rewrite the whole thing on my own

This was, of course, waaaay easier said than done. Mainly cause to rewrite the code on my own I needed to understand everything that went into my codebase. But that was always the plan, soooo, the rabbit hole it was.

First thing I did was look at the whole codebase, and try to make out at least major things that it used. This turned out to be the following:

  • Hardware
    • Pins
      • Pwm
      • Digital
      • etc
    • Speakers
      • Native speakers for testing on my laptop
      • Pinout speaker when running on my rasberry pi
    • Servo motors
    • Soon display modules as well
  • Personality
    • Mood definitions
    • Engine that controls changes to the mood
  • Voice
    • Sounds for letters
    • Speech module to convert sentences into letters and then sounds

In the pin_output for the sound, it really bothered me that it did both sound conversion and PWM initialization. So the first order of business was to remove pwm init from the speaker output module.

The second thing I did was basically spending an hour or two just understanding what the hell is PWM, how it works, and why it’s used for speakers. Turns out, that was a bit of a headache as well. So, PWM - Pulse width modulation - is a way to simulate analog signals using bursts of digital signals. Since digital signals have two values: high and low, or basically 1 and 0, while analog can have any value between 0 and 1 (if looking on a digital scale). PWM sends bursts of digital signal in rapid cycles, which a filter downstream then smooths into something that behaves like an analog value.

Let’s look at the new code:

// pwm.go in package pin
func NewPWM(pin int, cycleLength uint32, frequency int) *PWM {
	pwm := &PWM{rpio.Pin(pin), cycleLength, frequency}
	pwm.pin.Mode(rpio.Pwm)
	pwm.pin.Freq(frequency)
	return pwm
}

This will mainly be a reference for later. Where we just need to know which params is where.


const sampleRate = 44100
const cycleLength = 1024

func newOutput(pinIndex int) output {
	if os.Getenv("USE_PINS") == "true" {
		return newPinOutput(pin.NewPWM(pinIndex, cycleLength, sampleRate*cycleLength))
	}
	return newNativeOutput()
}

Init looks similar to what it did before, but it’s a bit easier to see what I’m dealing with. PWM index is set in the hardware.go file, so that I can see which pin is being dedicated to what use. This helps me reduce overhead of checking or memorizing pin usages.

Cycle length is set to 1024, which is a standard value used for PWM signals. Why 1024? Because it’s a nice 2^10 value, and 1024 potential values does give a clear reading. Using more than 1024 for cycle length can have drawbacks, but I haven’t gone far with looking into those.

The frequency is set to 44.100*1024, or cycle length * sample rate. This is because speaker expects input frequency of 44.100 Hz, or basically, 44.100 samples per second. With each sample requiring 1024 resolution steps, but tbh, in my brain it’s stuck as: 1 cycle having 1024 steps, and each second is composed of 44.100 cycles (1 cycle per sample). This isn’t technically correct, but it simplifies things for me. :D

Finally we get to the pin_output.go file:

func (speaker *pinOutput) normalizeCycleValue(buffer [2]float64, cycleLength uint32) uint32 {
	sample := (buffer[leftBufferIndex] + buffer[rightBufferIndex]) * 0.5
	// Shift value from [-1,1] range to [0, 1] range
	shiftedValue := (sample + 1.0) * 0.5
	return uint32(shiftedValue*float64(cycleLength) + 0.5)
}

func (speaker *pinOutput) playSound(streamer beep.Streamer) {
	cycleLength := speaker.pwm.CycleLength()
	samplePeriod := time.Second / time.Duration(sampleRate)

	buffer := make([][2]float64, 256)

	for {
		n, ok := streamer.Stream(buffer)
		for i := 0; i < n; i++ {
			speaker.pwm.SetDutyCycle(speaker.normalizeCycleValue(buffer[i], cycleLength))
			time.Sleep(samplePeriod)
		}
		if !ok {
			break
		}
	}
	speaker.pwm.SetDutyCycle(cycleLength / 2)
}

It’s again, pretty much the same. But normalization is extracted into a separate function, which makes it a lot more readable. Sound lib that I used streams values in range of [-1,1]. We can then normalize it to [0,1] range, and multiply it by cycle length to get the PWM value. Then it’s just a matter of setting the duty cycle every 1/44100 seconds.

Summary

There was a bunch of other stuff I don’t wanna go into here, but the link to the repo is in the navbar, so you can check it out.

But the most important part is: I’m finally happy working with the code on this project again! I know that today, it’s normal or even expected for AI to write most of the code, and that’s totally fine. But I like writing it myself and, while frustrating at times, I’m having fun doing so.