-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add 'irremote' package as a basic infra-red driver
Currently contains an IR Receiver device only. An IR Sender device will be added shortly Supports receiving only NEC (extended address) protocol including repeat codes so far
- Loading branch information
1 parent
edfdd4e
commit 01fed47
Showing
4 changed files
with
287 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package main | ||
|
||
import ( | ||
"machine" | ||
"time" | ||
|
||
"tinygo.org/x/drivers/irremote" | ||
) | ||
|
||
var irCmdButtons = map[uint16]string{ | ||
0xA2: "POWER", | ||
0xE2: "FUNC/STOP", | ||
0x62: "VOL+", | ||
0x22: "FAST BACK", | ||
0x02: "PAUSE", | ||
0xC2: "FAST FORWARD", | ||
0xE0: "DOWN", | ||
0xA8: "VOL-", | ||
0x90: "UP", | ||
0x98: "EQ", | ||
0xB0: "ST/REPT", | ||
0x68: "0", | ||
0x30: "1", | ||
0x18: "2", | ||
0x7A: "3", | ||
0x10: "4", | ||
0x38: "5", | ||
0x5A: "6", | ||
0x42: "7", | ||
0x4A: "8", | ||
0x52: "9", | ||
} | ||
|
||
var ( | ||
pinIRIn = machine.GP26 | ||
ir irremote.ReceiverDevice | ||
) | ||
|
||
func setupPins() { | ||
ir = irremote.NewReceiver(pinIRIn) | ||
ir.Configure() | ||
} | ||
|
||
func irCallback(data irremote.Data) { | ||
msg := "Command: " + irCmdButtons[data.Command] | ||
if data.Flags&irremote.DataFlagIsRepeat != 0 { | ||
msg = msg + " (REPEAT)" | ||
} | ||
println(msg) | ||
} | ||
|
||
func main() { | ||
setupPins() | ||
ir.SetCommandHandler(irCallback) | ||
for { | ||
time.Sleep(time.Millisecond * 10) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
package irremote // import "tinygo.org/x/drivers/irremote" | ||
|
||
import ( | ||
"machine" | ||
"time" | ||
) | ||
|
||
// NEC protocol references | ||
// https://www.sbprojects.net/knowledge/ir/nec.php | ||
// https://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol | ||
// https://simple-circuit.com/arduino-nec-remote-control-decoder/ | ||
|
||
// Data encapsulates the data received by the ReceiverDevice. | ||
type Data struct { | ||
// Code is the raw IR data received. | ||
Code uint32 | ||
// Address is the decoded address from the IR data received. | ||
Address uint16 | ||
// Command is the decoded command from the IR data recieved | ||
Command uint16 | ||
// Flags provides additional information about the IR data received. See DataFlags | ||
Flags DataFlags | ||
} | ||
|
||
// DataFlags provides bitwise flags representing various information about recieved IR data. | ||
type DataFlags uint16 | ||
|
||
// Valid values for DataFlags | ||
const ( | ||
// DataFlagIsRepeat set indicates that the IR data is a repeat commmand | ||
DataFlagIsRepeat DataFlags = 1 << iota | ||
) | ||
|
||
// CommandHandler defines the callback function used to provide IR data received by the ReceiverDevice. | ||
type CommandHandler func(data Data) | ||
|
||
// nec_ir_state represents the various internal states used to decode the NEC IR protocol commands | ||
type nec_ir_state uint8 | ||
|
||
// Valid values for nec_ir_state | ||
const ( | ||
lead_pulse_start nec_ir_state = iota // Start receiving IR data, beginning of 9ms lead pulse | ||
lead_space_start // End of 9ms lead pulse, start of 4.5ms space | ||
lead_space_end // End of 4.5ms space, start of 562µs pulse | ||
bit_read_start // End of 562µs pulse, start of 562µs or 1687µs space | ||
bit_read_end // End of 562µs or 1687µs space | ||
trail_pulse_end // End of 562µs trailing pulse | ||
) | ||
|
||
// ReceiverDevice is the device for receiving IR commands | ||
type ReceiverDevice struct { | ||
pin machine.Pin // IR input pin. | ||
ch CommandHandler // client callback function | ||
necState nec_ir_state // internal state machine | ||
data Data // decoded data for client | ||
lastTime time.Time // used to track states | ||
bitIndex int // tracks which bit (0-31) of necCode is being read | ||
} | ||
|
||
// NewReceiver returns a new IR receiver device | ||
func NewReceiver(pin machine.Pin) ReceiverDevice { | ||
return ReceiverDevice{pin: pin} | ||
} | ||
|
||
// Configure configures the input pin for the IR receiver device | ||
func (ir *ReceiverDevice) Configure() { | ||
// The IR receiver sends logic HIGH when NOT receiving IR, and logic LOW when receiving IR | ||
ir.pin.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) | ||
} | ||
|
||
// SetCommandHandler is used to start or stop receiving IR commands via a callback function (pass nil to stop) | ||
func (ir *ReceiverDevice) SetCommandHandler(ch CommandHandler) { | ||
ir.ch = ch | ||
ir.resetStateMachine() | ||
if ch != nil { | ||
// Start monitoring IR output pin for changes | ||
ir.pin.SetInterrupt(machine.PinFalling|machine.PinRising, ir.pinChange) | ||
} else { | ||
// Stop monitoring IR output pin for changes | ||
ir.pin.SetInterrupt(0, nil) | ||
} | ||
} | ||
|
||
// Internal helper function to reset state machine on protocol failure | ||
func (ir *ReceiverDevice) resetStateMachine() { | ||
ir.data = Data{} | ||
ir.bitIndex = 0 | ||
ir.necState = lead_pulse_start | ||
} | ||
|
||
// Internal pin rising/falling edge interrupt handler | ||
func (ir *ReceiverDevice) pinChange(pin machine.Pin) { | ||
/* Currently TinyGo is sending machine.NoPin (0xff) for all pins, at least on RP2040 | ||
if pin != ir.pin { | ||
return // This is not the pin you're looking for | ||
} | ||
*/ | ||
now := time.Now() | ||
duration := now.Sub(ir.lastTime) | ||
ir.lastTime = now | ||
switch ir.necState { | ||
case lead_pulse_start: | ||
if !ir.pin.Get() { | ||
// IR is 'on' (pin is pulled high and sent low when IR is received) | ||
ir.necState = lead_space_start // move to next state | ||
} | ||
case lead_space_start: | ||
if duration > time.Microsecond*9500 || duration < time.Microsecond*8500 { | ||
// Invalid interval for 9ms lead pulse. Reset | ||
ir.resetStateMachine() | ||
} else { | ||
// 9ms lead pulse detected, move to next state | ||
ir.necState = lead_space_end | ||
} | ||
case lead_space_end: | ||
if duration > time.Microsecond*5000 || duration < time.Microsecond*1750 { | ||
// Invalid interval for 4.5ms lead space OR 2.25ms repeat space. Reset | ||
ir.resetStateMachine() | ||
} else { | ||
// 4.5ms lead space OR 2.25ms repeat space detected | ||
if duration > time.Microsecond*3000 { | ||
// 4.5ms lead space detected, new code incoming, move to next state | ||
ir.resetStateMachine() | ||
ir.necState = bit_read_start | ||
} else { | ||
// 2.25ms repeat space detected. | ||
if ir.data.Code != 0 { | ||
// Valid repeat code. Invoke client callback with repeat flag set | ||
ir.data.Flags |= DataFlagIsRepeat | ||
if ir.ch != nil { | ||
ir.ch(ir.data) | ||
} | ||
ir.necState = lead_pulse_start | ||
} else { | ||
// ir.data is not in a valid state for a repeat. Reset | ||
ir.resetStateMachine() | ||
} | ||
} | ||
} | ||
case bit_read_start: | ||
if duration > time.Microsecond*700 || duration < time.Microsecond*400 { | ||
// Invalid interval for 562.5µs pulse. Reset | ||
ir.resetStateMachine() | ||
} else { | ||
// 562.5µs pulse detected, move to next state | ||
ir.necState = bit_read_end | ||
} | ||
case bit_read_end: | ||
if duration > time.Microsecond*1800 || duration < time.Microsecond*400 { | ||
// Invalid interval for 562.5µs space OR 1687.5µs space. Reset | ||
ir.resetStateMachine() | ||
} else { | ||
// 562.5µs OR 1687.5µs space detected | ||
mask := uint32((1 << (31 - ir.bitIndex))) | ||
if duration > time.Microsecond*1000 { | ||
// 1687.5µs space detected (logic 1) - Set bit | ||
ir.data.Code |= mask | ||
} else { | ||
// 562.5µs space detected (logic 0) - Clear bit | ||
ir.data.Code &^= mask | ||
} | ||
|
||
ir.bitIndex++ | ||
if ir.bitIndex > 31 { | ||
// We've read all bits for this code, move to next state | ||
ir.necState = trail_pulse_end | ||
} else { | ||
// Read next bit | ||
ir.necState = bit_read_start | ||
} | ||
} | ||
case trail_pulse_end: | ||
if duration > time.Microsecond*700 || duration < time.Microsecond*400 { | ||
// Invalid interval for trailing 562.5µs pulse. Reset | ||
ir.resetStateMachine() | ||
} else { | ||
// 562.5µs trailing pulse detected. Decode & validate data | ||
err := ir.decode() | ||
if err == irDecodeErrorNone { | ||
// Valid data, invoke client callback | ||
if ir.ch != nil { | ||
ir.ch(ir.data) | ||
} | ||
// around we go again. Note: we don't resetStateMachine() since repeat codes are now possible | ||
ir.necState = lead_pulse_start | ||
} else { | ||
ir.resetStateMachine() | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Error type for NEC format decoding | ||
type irDecodeError int | ||
|
||
// Valid values for irDecodeError | ||
const ( | ||
irDecodeErrorNone irDecodeError = iota // no error occurred | ||
irDecodeErrorInverseCheckFail // validation of inverse cmd does not match cmd | ||
) | ||
|
||
func (ir *ReceiverDevice) decode() irDecodeError { | ||
// Decode cmd and inverse cmd and perform validation check | ||
cmd := uint8((ir.data.Code & 0xff00) >> 8) | ||
invCmd := uint8(ir.data.Code & 0xff) | ||
if cmd != ^invCmd { | ||
// Validation failure. cmd and inverse cmd do not match | ||
return irDecodeErrorInverseCheckFail | ||
} | ||
// cmd validation pass, decode address | ||
ir.data.Command = uint16(cmd) | ||
addrLow := uint8((ir.data.Code & 0xff000000) >> 24) | ||
addrHigh := uint8((ir.data.Code & 0x00ff0000) >> 16) | ||
if addrHigh == ^addrLow { | ||
// addrHigh is inverse of addrLow. This is not a valid 16-bit address in extended NEC coding | ||
// since it is indistinguishable from 8-bit address with inverse validation. Use the 8-bit address | ||
ir.data.Address = uint16(addrLow) | ||
} else { | ||
// 16-bit extended NEC address | ||
ir.data.Address = (uint16(addrHigh) << 8) | uint16(addrLow) | ||
} | ||
// Clear repeat flag | ||
ir.data.Flags &^= DataFlagIsRepeat | ||
return irDecodeErrorNone | ||
} |