IR-controlled A/C (7/many) - Modulating

4 minute read

To summarize a bit, I’m down to the point where I’m going to use the pigpio library to drive the GPIO directly instead of using lirc.

Talking to libgpio

I continued being stubborn and wanting to do things in Go. The library is written in C, has python bindings, but nothing for Go, and I was also too lazy to do a CGo binding (it’s totally doable and not that hard, but it’s long).

Conveniently enough there is also a daemon called pigpiod with a network interface. All I need to do is dial localhost! And so off we go. I pretty much reimplemented the daikin-pi project in order to recreate the full fame, except that instead of writing out a file, I would connect to the pigpiod socket, and send the command.

Here’s the example of the first part of the frame.

// [...]
type byteStream []uint8

// [...]
func frame1() (byteStream, error) {
	frame := byteStream(make([]uint8, 8))
	frame.init() // Skipping return check is ok here

	frame[4] = 0xc5 // Message ID
	// frame[5] unused
	// frame[6] is 0x10 for comfort mode, skipping for now.
	frame[7] = frameChecksum(frame)
	return frame, nil
}

As we’ll see, in order to talk to pigpiod there was a little bit of learning and a little bit of frustration.

Learning

From the documentation it’s not always super clear what to send. For example, to start a new wave, the C function is gpioWaveAddNew (documentation) and you need to find in the list of codes that it is command WVNEW ie 53.

Once that was clear, sending commands was easy

func fillCommand(cmd []byte, b1, b2, b3, b4 uint32) {
	if cap(cmd) < 4 {
		log.Fatal("not enoug space")
	}
	binary.LittleEndian.PutUint32(cmd, b1)
	binary.LittleEndian.PutUint32(cmd[4:], b2)
	binary.LittleEndian.PutUint32(cmd[8:], b3)
	binary.LittleEndian.PutUint32(cmd[12:], b4)
}

func sendSimpleCommand(conn net.Conn, b1, b2, b3, b4 uint32) (uint32, error) {
	cmd := make([]byte, 4*4)
	fillCommand(cmd, b1, b2, b3, b4)
	return sendCommand(conn, cmd)
}

Frustration

At the pigpio dameon

There were a few shortcomings in the documentation.

The first one was around then endianness of the uint32 fields, or even how to properly send a structure through a TCP connection. I assumed just put one uint32 after another, that was right.

I had assumed bigendian, and that was wrong (I modified the snippet above after I figured that out)

At myself

So after a bit of work, I think I have it. I look at piscope and the timings are right. Hurray!.

And so I point the LED at the A/C unit and ….. nada. Over and over and over.

At that point, I was ready to give it up. Until my brother sent me a link to this video (in French Québécois), where the creator explains a specific IR protocol … and highlights something obvious that I should totally have remembered: the signal has to be modulated !!!

What even more frustraing is that zooming in on the piscope window, I had seen it!

Piscope showing the modulation

I just had totally forgotten 😡😡😡

At PWM

PWM should have been a way to modulate. I just could never figure it out. I don’t remember fully what I tried, but it didn’t work.

At this point, I had done everything very manually, I might as well “modulate” manually! Instead of sending X ms of “high” signal, I would just “cut” it, ie. alternate high an low state.

Here’s how it works. Some functions to get from bytes to the “high” and “low” state durations.

// Directly from the gpiod commands
type gpioConf struct {
	on      uint32
	off     uint32
	delayUs uint32
}

type byteStream []uint8 // The bytes to send to the A/C unit
type pulseStream []uint32 // Alternating time up time down
type modPulseStream []gpioConf // The result: a list of gpio commands


// Transforms a series of bytes into a pulseStream
// Effectively how 0x11 becomes 	430 1320 430 430 430 430 430 430 etc.
func (s byteStream) pulses() pulseStream {
	var pulses = make(pulseStream, 2*8*len(s))
	for i, b := range s {
		for bit := 0; bit < 8; bit++ {
			pulses[16*i+2*bit] = pulseHigh
			// Right to left encoding.
			if (b >> bit & 0x1) == 0 {
				pulses[16*i+2*bit+1] = pulseLowZero
			} else {
				pulses[16*i+2*bit+1] = pulseLowOne
			}
		}
	}
	return pulses
}

And how to modulate it. pulseTimeMicro is initialized based on a 38kHz frequency. And you need to divide by 2 because I need 2 states per cycle.

var (
	freq           = 38000
	pulseTimeMicro = uint32(1000 * 1000 / freq / 2)
)

func (p pulseStream) modulate() modPulseStream {
	var ms modPulseStream
	for i, dur := range p {
		// even number is high -> have to modulate
		if i%2 == 0 {
			nbPulse := 1 + (dur-1)/pulseTimeMicro
			remainingDur := dur
			for j := uint32(0); j < nbPulse; j++ {
				actualDur := pulseTimeMicro
				if actualDur > remainingDur {
					actualDur = remainingDur
				}
				remainingDur -= actualDur
				if j%2 == 0 {
					ms = append(ms, gpioConf{
						on:      1 << gpio,
						off:     0,
						delayUs: actualDur,
					})
				} else {
					ms = append(ms, gpioConf{
						on:      0,
						off:     1 << gpio,
						delayUs: actualDur,
					})
				}
			}
		} else {
			ms = append(ms, gpioConf{
				on:      0,
				off:     1 << gpio,
				delayUs: dur,
			})
		}
	}
	return ms
}

At the dameon again

Turns out there’s a maximum command length you can send over the socket! It might be documented somewhere, but I didn’t find it.

Luckily there’s a command called WVAG which corresponds to gpioWaveAddGeneric, and by trying values, I found that the maximum message size seemed to be aroudn 65000 bytes, I’m ready to bet it’s actually around 65535…

At that point, I had probably gone as far as I would ever be ready to go.