IR-controlled A/C (7/many) - Modulating
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!
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.