|
| 1 | +package irremote // import "tinygo.org/x/drivers/irremote" |
| 2 | + |
| 3 | +import ( |
| 4 | + "machine" |
| 5 | + "time" |
| 6 | +) |
| 7 | + |
| 8 | +// NEC protocol references |
| 9 | +// https://www.sbprojects.net/knowledge/ir/nec.php |
| 10 | +// https://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol |
| 11 | +// https://simple-circuit.com/arduino-nec-remote-control-decoder/ |
| 12 | + |
| 13 | +// Data encapsulates the data received by the ReceiverDevice. |
| 14 | +type Data struct { |
| 15 | + // Code is the raw IR data received. |
| 16 | + Code uint32 |
| 17 | + // Address is the decoded address from the IR data received. |
| 18 | + Address uint16 |
| 19 | + // Command is the decoded command from the IR data recieved |
| 20 | + Command uint16 |
| 21 | + // Flags provides additional information about the IR data received. See DataFlags |
| 22 | + Flags DataFlags |
| 23 | +} |
| 24 | + |
| 25 | +// DataFlags provides bitwise flags representing various information about recieved IR data. |
| 26 | +type DataFlags uint16 |
| 27 | + |
| 28 | +// Valid values for DataFlags |
| 29 | +const ( |
| 30 | + // DataFlagIsRepeat set indicates that the IR data is a repeat commmand |
| 31 | + DataFlagIsRepeat DataFlags = 1 << iota |
| 32 | +) |
| 33 | + |
| 34 | +// CommandHandler defines the callback function used to provide IR data received by the ReceiverDevice. |
| 35 | +type CommandHandler func(data Data) |
| 36 | + |
| 37 | +// nec_ir_state represents the various internal states used to decode the NEC IR protocol commands |
| 38 | +type nec_ir_state uint8 |
| 39 | + |
| 40 | +// Valid values for nec_ir_state |
| 41 | +const ( |
| 42 | + lead_pulse_start nec_ir_state = iota // Start receiving IR data, beginning of 9ms lead pulse |
| 43 | + lead_space_start // End of 9ms lead pulse, start of 4.5ms space |
| 44 | + lead_space_end // End of 4.5ms space, start of 562µs pulse |
| 45 | + bit_read_start // End of 562µs pulse, start of 562µs or 1687µs space |
| 46 | + bit_read_end // End of 562µs or 1687µs space |
| 47 | + trail_pulse_end // End of 562µs trailing pulse |
| 48 | +) |
| 49 | + |
| 50 | +// ReceiverDevice is the device for receiving IR commands |
| 51 | +type ReceiverDevice struct { |
| 52 | + pin machine.Pin // IR input pin. |
| 53 | + ch CommandHandler // client callback function |
| 54 | + necState nec_ir_state // internal state machine |
| 55 | + data Data // decoded data for client |
| 56 | + lastTime time.Time // used to track states |
| 57 | + bitIndex int // tracks which bit (0-31) of necCode is being read |
| 58 | +} |
| 59 | + |
| 60 | +// NewReceiver returns a new IR receiver device |
| 61 | +func NewReceiver(pin machine.Pin) ReceiverDevice { |
| 62 | + return ReceiverDevice{pin: pin} |
| 63 | +} |
| 64 | + |
| 65 | +// Configure configures the input pin for the IR receiver device |
| 66 | +func (ir *ReceiverDevice) Configure() { |
| 67 | + // The IR receiver sends logic HIGH when NOT receiving IR, and logic LOW when receiving IR |
| 68 | + ir.pin.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) |
| 69 | +} |
| 70 | + |
| 71 | +// SetCommandHandler is used to start or stop receiving IR commands via a callback function (pass nil to stop) |
| 72 | +func (ir *ReceiverDevice) SetCommandHandler(ch CommandHandler) { |
| 73 | + ir.ch = ch |
| 74 | + ir.resetStateMachine() |
| 75 | + if ch != nil { |
| 76 | + // Start monitoring IR output pin for changes |
| 77 | + ir.pin.SetInterrupt(machine.PinFalling|machine.PinRising, ir.pinChange) |
| 78 | + } else { |
| 79 | + // Stop monitoring IR output pin for changes |
| 80 | + ir.pin.SetInterrupt(0, nil) |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +// Internal helper function to reset state machine on protocol failure |
| 85 | +func (ir *ReceiverDevice) resetStateMachine() { |
| 86 | + ir.data = Data{} |
| 87 | + ir.bitIndex = 0 |
| 88 | + ir.necState = lead_pulse_start |
| 89 | +} |
| 90 | + |
| 91 | +// Internal pin rising/falling edge interrupt handler |
| 92 | +func (ir *ReceiverDevice) pinChange(pin machine.Pin) { |
| 93 | + /* Currently TinyGo is sending machine.NoPin (0xff) for all pins, at least on RP2040 |
| 94 | + if pin != ir.pin { |
| 95 | + return // This is not the pin you're looking for |
| 96 | + } |
| 97 | + */ |
| 98 | + now := time.Now() |
| 99 | + duration := now.Sub(ir.lastTime) |
| 100 | + ir.lastTime = now |
| 101 | + switch ir.necState { |
| 102 | + case lead_pulse_start: |
| 103 | + if !ir.pin.Get() { |
| 104 | + // IR is 'on' (pin is pulled high and sent low when IR is received) |
| 105 | + ir.necState = lead_space_start // move to next state |
| 106 | + } |
| 107 | + case lead_space_start: |
| 108 | + if duration > time.Microsecond*9500 || duration < time.Microsecond*8500 { |
| 109 | + // Invalid interval for 9ms lead pulse. Reset |
| 110 | + ir.resetStateMachine() |
| 111 | + } else { |
| 112 | + // 9ms lead pulse detected, move to next state |
| 113 | + ir.necState = lead_space_end |
| 114 | + } |
| 115 | + case lead_space_end: |
| 116 | + if duration > time.Microsecond*5000 || duration < time.Microsecond*1750 { |
| 117 | + // Invalid interval for 4.5ms lead space OR 2.25ms repeat space. Reset |
| 118 | + ir.resetStateMachine() |
| 119 | + } else { |
| 120 | + // 4.5ms lead space OR 2.25ms repeat space detected |
| 121 | + if duration > time.Microsecond*3000 { |
| 122 | + // 4.5ms lead space detected, new code incoming, move to next state |
| 123 | + ir.resetStateMachine() |
| 124 | + ir.necState = bit_read_start |
| 125 | + } else { |
| 126 | + // 2.25ms repeat space detected. |
| 127 | + if ir.data.Code != 0 { |
| 128 | + // Valid repeat code. Invoke client callback with repeat flag set |
| 129 | + ir.data.Flags |= DataFlagIsRepeat |
| 130 | + if ir.ch != nil { |
| 131 | + ir.ch(ir.data) |
| 132 | + } |
| 133 | + ir.necState = lead_pulse_start |
| 134 | + } else { |
| 135 | + // ir.data is not in a valid state for a repeat. Reset |
| 136 | + ir.resetStateMachine() |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + case bit_read_start: |
| 141 | + if duration > time.Microsecond*700 || duration < time.Microsecond*400 { |
| 142 | + // Invalid interval for 562.5µs pulse. Reset |
| 143 | + ir.resetStateMachine() |
| 144 | + } else { |
| 145 | + // 562.5µs pulse detected, move to next state |
| 146 | + ir.necState = bit_read_end |
| 147 | + } |
| 148 | + case bit_read_end: |
| 149 | + if duration > time.Microsecond*1800 || duration < time.Microsecond*400 { |
| 150 | + // Invalid interval for 562.5µs space OR 1687.5µs space. Reset |
| 151 | + ir.resetStateMachine() |
| 152 | + } else { |
| 153 | + // 562.5µs OR 1687.5µs space detected |
| 154 | + mask := uint32((1 << (31 - ir.bitIndex))) |
| 155 | + if duration > time.Microsecond*1000 { |
| 156 | + // 1687.5µs space detected (logic 1) - Set bit |
| 157 | + ir.data.Code |= mask |
| 158 | + } else { |
| 159 | + // 562.5µs space detected (logic 0) - Clear bit |
| 160 | + ir.data.Code &^= mask |
| 161 | + } |
| 162 | + |
| 163 | + ir.bitIndex++ |
| 164 | + if ir.bitIndex > 31 { |
| 165 | + // We've read all bits for this code, move to next state |
| 166 | + ir.necState = trail_pulse_end |
| 167 | + } else { |
| 168 | + // Read next bit |
| 169 | + ir.necState = bit_read_start |
| 170 | + } |
| 171 | + } |
| 172 | + case trail_pulse_end: |
| 173 | + if duration > time.Microsecond*700 || duration < time.Microsecond*400 { |
| 174 | + // Invalid interval for trailing 562.5µs pulse. Reset |
| 175 | + ir.resetStateMachine() |
| 176 | + } else { |
| 177 | + // 562.5µs trailing pulse detected. Decode & validate data |
| 178 | + err := ir.decode() |
| 179 | + if err == irDecodeErrorNone { |
| 180 | + // Valid data, invoke client callback |
| 181 | + if ir.ch != nil { |
| 182 | + ir.ch(ir.data) |
| 183 | + } |
| 184 | + // around we go again. Note: we don't resetStateMachine() since repeat codes are now possible |
| 185 | + ir.necState = lead_pulse_start |
| 186 | + } else { |
| 187 | + ir.resetStateMachine() |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +// Error type for NEC format decoding |
| 194 | +type irDecodeError int |
| 195 | + |
| 196 | +// Valid values for irDecodeError |
| 197 | +const ( |
| 198 | + irDecodeErrorNone irDecodeError = iota // no error occurred |
| 199 | + irDecodeErrorInverseCheckFail // validation of inverse cmd does not match cmd |
| 200 | +) |
| 201 | + |
| 202 | +func (ir *ReceiverDevice) decode() irDecodeError { |
| 203 | + // Decode cmd and inverse cmd and perform validation check |
| 204 | + cmd := uint8((ir.data.Code & 0xff00) >> 8) |
| 205 | + invCmd := uint8(ir.data.Code & 0xff) |
| 206 | + if cmd != ^invCmd { |
| 207 | + // Validation failure. cmd and inverse cmd do not match |
| 208 | + return irDecodeErrorInverseCheckFail |
| 209 | + } |
| 210 | + // cmd validation pass, decode address |
| 211 | + ir.data.Command = uint16(cmd) |
| 212 | + addrLow := uint8((ir.data.Code & 0xff000000) >> 24) |
| 213 | + addrHigh := uint8((ir.data.Code & 0x00ff0000) >> 16) |
| 214 | + if addrHigh == ^addrLow { |
| 215 | + // addrHigh is inverse of addrLow. This is not a valid 16-bit address in extended NEC coding |
| 216 | + // since it is indistinguishable from 8-bit address with inverse validation. Use the 8-bit address |
| 217 | + ir.data.Address = uint16(addrLow) |
| 218 | + } else { |
| 219 | + // 16-bit extended NEC address |
| 220 | + ir.data.Address = (uint16(addrHigh) << 8) | uint16(addrLow) |
| 221 | + } |
| 222 | + // Clear repeat flag |
| 223 | + ir.data.Flags &^= DataFlagIsRepeat |
| 224 | + return irDecodeErrorNone |
| 225 | +} |
0 commit comments