Files
MultiRoombaRover/pi/roverd/ir_tx.go
T

328 lines
7.2 KiB
Go

//go:build !dummy
package roverd
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"sync"
"time"
)
const (
pigpioAddr = "127.0.0.1:8888"
piCmdSetMode = 0
piCmdWrite = 4
piCmdWaveClear = 27
piCmdWaveAddGeneric = 28
piCmdWaveTxBusy = 32
piCmdWaveCreate = 49
piCmdWaveDelete = 50
piCmdWaveTxSend = 51
piOutput = 1
)
type pigpioCmd struct {
Cmd uint32
P1 uint32
P2 uint32
P3 uint32
}
type gpioPulse struct {
GpioOn uint32
GpioOff uint32
DelayUs uint32
}
type pigpioClient struct {
conn net.Conn
mu sync.Mutex
}
func newPigpioClient(addr string) (*pigpioClient, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
if tcpConn, ok := conn.(*net.TCPConn); ok {
_ = tcpConn.SetNoDelay(true)
}
return &pigpioClient{conn: conn}, nil
}
func (c *pigpioClient) Close() error {
if c.conn == nil {
return nil
}
return c.conn.Close()
}
func (c *pigpioClient) command(cmd, p1, p2, p3 uint32, ext []byte) (int32, error) {
c.mu.Lock()
defer c.mu.Unlock()
var buf [16]byte
binary.LittleEndian.PutUint32(buf[0:], cmd)
binary.LittleEndian.PutUint32(buf[4:], p1)
binary.LittleEndian.PutUint32(buf[8:], p2)
binary.LittleEndian.PutUint32(buf[12:], p3)
if _, err := c.conn.Write(buf[:]); err != nil {
return -1, err
}
if len(ext) > 0 {
if _, err := c.conn.Write(ext); err != nil {
return -1, err
}
}
if _, err := io.ReadFull(c.conn, buf[:]); err != nil {
return -1, err
}
res := int32(binary.LittleEndian.Uint32(buf[12:]))
return res, nil
}
type IRTransmitter struct {
cfg IRConfig
logger *log.Logger
gpioMask uint32
pigpio *pigpioClient
mu sync.Mutex
closed bool
activeLow bool
}
func NewIRTransmitter(cfg IRConfig, logger *log.Logger) (*IRTransmitter, error) {
if !cfg.Enabled {
return nil, fmt.Errorf("ir disabled")
}
client, err := newPigpioClient(pigpioAddr)
if err != nil {
return nil, fmt.Errorf("connect pigpio: %w", err)
}
mask := uint32(1) << cfg.Pin
tx := &IRTransmitter{
cfg: cfg,
logger: logger,
gpioMask: mask,
pigpio: client,
activeLow: cfg.ActiveLow,
}
if err := tx.configureLine(); err != nil {
_ = client.Close()
return nil, err
}
logger.Printf("ir tx initialized on GPIO %d (%d Hz carrier, activeLow=%v)", cfg.Pin, cfg.CarrierHz, tx.activeLow)
return tx, nil
}
func (t *IRTransmitter) Close() {
t.mu.Lock()
defer t.mu.Unlock()
if t.closed {
return
}
_ = t.setInactive()
_ = t.pigpio.Close()
t.closed = true
}
func (t *IRTransmitter) Send(code byte, repeat int) error {
t.mu.Lock()
defer t.mu.Unlock()
if t.closed {
return fmt.Errorf("ir transmitter closed")
}
if repeat <= 0 {
repeat = t.cfg.Repeat
}
pulses, totalUs := t.buildWaveform(code, repeat)
if len(pulses) == 0 {
return nil
}
if err := t.writeWave(pulses, time.Duration(totalUs)*time.Microsecond); err != nil {
return err
}
return nil
}
func (t *IRTransmitter) configureLine() error {
if res, err := t.pigpio.command(piCmdSetMode, uint32(t.cfg.Pin), piOutput, 0, nil); err != nil {
return fmt.Errorf("pigpio set mode: %w", err)
} else if res < 0 {
return fmt.Errorf("pigpio set mode: %d", res)
}
return t.setInactive()
}
func (t *IRTransmitter) setInactive() error {
level := uint32(0)
if t.activeLow {
level = 1
}
res, err := t.pigpio.command(piCmdWrite, uint32(t.cfg.Pin), level, 0, nil)
if err != nil {
return fmt.Errorf("pigpio write: %w", err)
}
if res < 0 {
return fmt.Errorf("pigpio write: %d", res)
}
return nil
}
func (t *IRTransmitter) buildWaveform(code byte, repeat int) ([]gpioPulse, int) {
if repeat <= 0 {
return nil, 0
}
periodUs := int(1_000_000 / t.cfg.CarrierHz)
if periodUs <= 0 {
periodUs = 1
}
onDuty := int(float64(periodUs) * (float64(t.cfg.DutyPercent) / 100.0))
if onDuty <= 0 {
onDuty = 1
}
if onDuty >= periodUs {
onDuty = periodUs - 1
}
offDuty := periodUs - onDuty
if offDuty <= 0 {
offDuty = 1
}
var pulses []gpioPulse
totalUs := 0
onPulse := func(duration int) {
if duration <= 0 {
return
}
pulses = append(pulses, t.pulseOn(duration))
totalUs += duration
}
offPulse := func(duration int) {
if duration <= 0 {
return
}
pulses = append(pulses, t.pulseOff(duration))
totalUs += duration
}
addCarrier := func(onUs int) {
if onUs <= 0 {
return
}
cycles := onUs / periodUs
if onUs%periodUs != 0 {
cycles++
}
for i := 0; i < cycles; i++ {
onPulse(onDuty)
offPulse(offDuty)
}
}
for i := 0; i < repeat; i++ {
for mask := byte(0x80); mask > 0; mask >>= 1 {
onMs := t.cfg.Bit0OnMs
if code&mask != 0 {
onMs = t.cfg.Bit1OnMs
}
onUs := onMs * 1000
offUs := (t.cfg.BitTotalMs - onMs) * 1000
addCarrier(onUs)
offPulse(offUs)
}
if i < repeat-1 && t.cfg.GapMs > 0 {
offPulse(t.cfg.GapMs * 1000)
}
}
if totalUs > 0 {
pulses = append(pulses, t.pulseOff(1))
totalUs++
}
return pulses, totalUs
}
func (t *IRTransmitter) pulseOn(durationUs int) gpioPulse {
if t.activeLow {
return gpioPulse{GpioOn: 0, GpioOff: t.gpioMask, DelayUs: uint32(durationUs)}
}
return gpioPulse{GpioOn: t.gpioMask, GpioOff: 0, DelayUs: uint32(durationUs)}
}
func (t *IRTransmitter) pulseOff(durationUs int) gpioPulse {
if t.activeLow {
return gpioPulse{GpioOn: t.gpioMask, GpioOff: 0, DelayUs: uint32(durationUs)}
}
return gpioPulse{GpioOn: 0, GpioOff: t.gpioMask, DelayUs: uint32(durationUs)}
}
func (t *IRTransmitter) writeWave(pulses []gpioPulse, duration time.Duration) error {
if len(pulses) == 0 {
return nil
}
if res, err := t.pigpio.command(piCmdWaveClear, 0, 0, 0, nil); err != nil {
return fmt.Errorf("pigpio wave clear: %w", err)
} else if res < 0 {
return fmt.Errorf("pigpio wave clear: %d", res)
}
payload := make([]byte, 0, len(pulses)*12)
buf := bytes.NewBuffer(payload)
for _, pulse := range pulses {
_ = binary.Write(buf, binary.LittleEndian, pulse.GpioOn)
_ = binary.Write(buf, binary.LittleEndian, pulse.GpioOff)
_ = binary.Write(buf, binary.LittleEndian, pulse.DelayUs)
}
data := buf.Bytes()
if res, err := t.pigpio.command(piCmdWaveAddGeneric, 0, 0, uint32(len(data)), data); err != nil {
return fmt.Errorf("pigpio wave add: %w", err)
} else if res < 0 {
return fmt.Errorf("pigpio wave add: %d", res)
}
waveID, err := t.pigpio.command(piCmdWaveCreate, 0, 0, 0, nil)
if err != nil {
return fmt.Errorf("pigpio wave create: %w", err)
}
if waveID < 0 {
return fmt.Errorf("pigpio wave create: %d", waveID)
}
defer func() {
_, _ = t.pigpio.command(piCmdWaveDelete, uint32(waveID), 0, 0, nil)
}()
if res, err := t.pigpio.command(piCmdWaveTxSend, uint32(waveID), 0, 0, nil); err != nil {
return fmt.Errorf("pigpio wave tx: %w", err)
} else if res < 0 {
return fmt.Errorf("pigpio wave tx: %d", res)
}
if duration <= 0 {
return nil
}
timeout := duration + 250*time.Millisecond
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
busy, err := t.pigpio.command(piCmdWaveTxBusy, 0, 0, 0, nil)
if err != nil {
return fmt.Errorf("pigpio wave busy: %w", err)
}
if busy < 0 {
return fmt.Errorf("pigpio wave busy: %d", busy)
}
if busy == 0 {
return nil
}
time.Sleep(2 * time.Millisecond)
}
return fmt.Errorf("pigpio wave timeout after %s", timeout)
}