You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/firmata/client.go

719 lines
17 KiB
Go

package firmata
import (
"bufio"
"errors"
"fmt"
"io"
"sync"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/onewire"
"periph.io/x/conn/v3/pin"
)
// These max values are for data bytes as, within firmata, data is 7 bits long.
const (
MaxUInt8 uint8 = (1<<8 - 1) >> 1
MaxUInt16 uint16 = (1<<16 - 1) >> 2
)
var commandResponseMap = map[SysExCmd]SysExCmd{
SysExAnalogMappingQuery: SysExAnalogMappingResponse,
SysExCapabilityQuery: SysExCapabilityResponse,
SysExPinStateQuery: SysExPinStateResponse,
}
type ClientI interface {
SendSysEx(SysExCmd, ...byte) (chan []byte, error)
SendReset() error
ExtendedReportAnalogPin(uint8, int) error
CapabilityQuery() (chan CapabilityResponse, error)
PinStateQuery(uint8) (chan PinStateResponse, error)
ReportFirmware() (chan FirmwareReport, error)
SetPinMode(uint8, pin.Func) error
SetAnalogPinReporting(uint8, bool) error
SetDigitalPinReporting(uint8, bool) error
SetDigitalPortReporting(uint8, bool) error
SetSamplingInterval(uint16) error
SetDigitalPinValue(p uint8, value gpio.Level) error
SendAnalogMappingQuery() (chan AnalogMappingResponse, error)
AnalogPinToDigitalPin(p uint8) (uint8, error)
SetAnalogIOMessageListener(p uint8, ch chan uint16) (release func(), err error)
SetDigitalIOMessageListener(p uint8, ch chan gpio.Level) (release func(), err error)
SendAnalogIOMessage(uint8, uint16) error
OpenI2CBus() (i2c.Bus, error)
SetI2CAddressListener(addr uint8, ch chan I2CPacket) (release func(), err error)
WriteI2CData(address uint8, restart bool, data []uint8) error
ReadI2CData(address uint8, restart bool, len uint16) error
ReadI2CRegister(address uint8, restart bool, register uint8, len uint16) error
SendI2CConfig(delayMicroseconds uint8) error
OpenOneWireBus(p uint8) (bus onewire.BusCloser, err error)
SetOneWireListener(uint8, chan []byte) (release func(), err error)
GetPinName(uint8) string
GetPinFunctions(uint8) []pin.Func
Close() error
}
type Client struct {
board io.ReadWriteCloser
responseChannels map[SysExCmd][]chan []byte
sysExListenerChannels map[SysExCmd]chan []byte
i2cListeners map[uint8]chan I2CPacket
i2cMU sync.Mutex
onewireListeners map[uint8]chan []byte
onewireMU sync.Mutex
digitalIOMessageChannels map[uint8]chan gpio.Level
digitalPinMU sync.Mutex
analogIOMessageChannels map[uint8]chan uint16
analogPinMU sync.Mutex
mu sync.Mutex
started bool
i2cStarted bool
// We want to report these to the requester, but also save them for internal use.
cr CapabilityResponse
amr AnalogMappingResponse
}
func NewClient(board io.ReadWriteCloser) *Client {
return &Client{
board: board,
responseChannels: map[SysExCmd][]chan []byte{},
sysExListenerChannels: map[SysExCmd]chan []byte{},
i2cListeners: map[uint8]chan I2CPacket{},
onewireListeners: map[uint8]chan []byte{},
digitalIOMessageChannels: map[uint8]chan gpio.Level{},
analogIOMessageChannels: map[uint8]chan uint16{},
}
}
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
c.started = false
return c.board.Close()
}
type flusher interface {
Flush()
}
type flusherErr interface {
Flush() error
}
func (c *Client) Start() error {
c.mu.Lock()
if c.started {
c.mu.Unlock()
return ErrAlreadyStarted
}
c.started = true
c.mu.Unlock()
if b, ok := c.board.(flusher); ok {
b.Flush()
} else if b, ok := c.board.(flusherErr); ok {
if err := b.Flush(); err != nil {
return err
}
}
firmChannel := make(chan []byte, 1)
// Don't call ReportFirmware as it is automatic, but we want to register a listener for it.
c.mu.Lock()
c.responseChannels[SysExReportFirmware] = []chan []byte{firmChannel}
c.mu.Unlock()
report := c.parseReportFirmware(firmChannel)
go func() {
err := c.responseWatcher()
if err != nil {
panic(err)
}
}()
fmt.Println("Firmware Info:", <-report)
return nil
}
func (c *Client) write(payload []byte, withinMutex func()) error {
// Cannot allow multiple writes at the same time.
c.mu.Lock()
defer c.mu.Unlock()
//fmt.Println(SprintHexArray(payload))
// Write to the board.
_, err := c.board.Write(payload)
if err != nil {
return err
}
if withinMutex != nil {
withinMutex()
}
return nil
}
func (c *Client) responseWatcher() (err error) {
defer func() {
if errors.Is(err, io.EOF) {
err = ErrDeviceDisconnected
}
}()
reader := bufio.NewReader(c.board)
for {
var data []byte
b0, err := reader.ReadByte()
if err != nil {
return err
}
mt := MessageType(b0)
switch {
case mt == ProtocolVersion:
var version [2]byte
_, err := reader.Read(version[:])
if err != nil {
return err
}
fmt.Printf("Protocol Version: 0x%0.2X 0x%0.2X\n", version[0], version[1])
case AnalogIOMessage <= mt && mt <= (AnalogIOMessage+0xF):
v1, err := reader.ReadByte()
if err != nil {
return err
}
v2, err := reader.ReadByte()
if err != nil {
return err
}
c.analogIOMessageChannels[b0&0xF] <- TwoByteToWord(v1, v2)
case DigitalIOMessage <= mt && mt <= (DigitalIOMessage+0xF):
v1, err := reader.ReadByte()
if err != nil {
return err
}
v2, err := reader.ReadByte()
if err != nil {
return err
}
values := TwoByteToByte(v1, v2)
port := b0 & 0xF
pinMin := port * 8
pinMax := (port+1)*8 - 1
for p := pinMin; p <= pinMax; p++ {
if ch, ok := c.digitalIOMessageChannels[p]; ok {
lvl := gpio.Low
if values>>p%8 > 0 {
lvl = gpio.High
}
ch <- lvl
}
}
case mt == StartSysEx:
data, err = reader.ReadBytes(byte(EndSysEx))
if err != nil {
return err
}
if len(data) == 0 {
return ErrNoDataRead
}
cmd := SysExCmd(data[0])
data = data[1 : len(data)-1]
switch {
case cmd == SysExSerialDataV1:
fallthrough
case cmd == SysExSerialDataV2:
return fmt.Errorf("%w: %s", ErrUnsupportedFeature, cmd)
case cmd == SysExOneWireData:
p := data[1]
if l, ok := c.onewireListeners[p]; ok {
l <- data
} else {
return fmt.Errorf("%w: onewire cmd:0x%02X pin 0x%02X", ErrUnhandledMessage, data[0], p)
}
case cmd == SysExI2CReply:
address := TwoByteToByte(data[0], data[1])
register := TwoByteToByte(data[2], data[3])
ch, ok := c.i2cListeners[address]
if !ok {
return fmt.Errorf("%w: 0x%02X", ErrNoI2CListenerForAddress, address)
}
ch <- I2CPacket{
Register: register,
Data: TwoByteRepresentationToByteSlice(data[4:]),
}
case c.sysExListenerChannels[cmd] != nil:
c.sysExListenerChannels[cmd] <- data
case len(c.responseChannels[cmd]) != 0:
c.mu.Lock()
resp := c.responseChannels[cmd][0]
c.responseChannels[cmd] = c.responseChannels[cmd]
c.mu.Unlock()
resp <- data
close(resp)
case cmd == SysExStringData:
fmt.Printf("device: [%s]\n", TwoByteString(data))
default:
str := ""
if cmd == SysExStringData {
str = TwoByteString(data)
} else {
for _, b := range data {
str += fmt.Sprintf("%d", b)
}
}
return fmt.Errorf("%w: 0x%0.2X: %s", ErrUnexpectedSysExMessageTypeReceived, byte(cmd), str)
}
default:
return fmt.Errorf("%w: 0x%0.2X", ErrInvalidMessageTypeStart, b0)
}
}
}
func (c *Client) SendReset() error {
return c.write([]byte{byte(SystemReset)}, nil)
}
func (c *Client) AnalogPinToDigitalPin(p uint8) (uint8, error) {
if int(p) > len(c.amr.AnalogPinToDigital) {
return 0, ErrInvalidAnalogPin
}
return c.amr.AnalogPinToDigital[p], nil
}
func (c *Client) SetPinMode(p uint8, mode pin.Func) error {
return c.write([]uint8{uint8(SetPinMode), p, pinFuncToModeMap[mode]}, nil)
}
func (c *Client) SetDigitalPinValue(p uint8, value gpio.Level) error {
v := byte(0)
if value {
v = 1
}
return c.write([]uint8{uint8(SetDigitalPinValue), p, v}, nil)
}
func (c *Client) SendSysEx(cmd SysExCmd, payload ...byte) (chan []byte, error) {
// Create a response channel.
var data chan []byte
err := c.write(append([]byte{byte(StartSysEx), byte(cmd)}, append(payload, byte(EndSysEx))...), func() {
// This assumes that SysEx commands of the same type are responded to in order.
if resp, ok := commandResponseMap[cmd]; ok {
data = make(chan []byte, 1)
c.responseChannels[resp] = append(c.responseChannels[resp], data)
}
})
if err != nil {
return nil, err
}
return data, nil
}
func (c *Client) CapabilityQuery() (chan CapabilityResponse, error) {
future, err := c.SendSysEx(SysExCapabilityQuery)
if err != nil {
return nil, err
}
resp := c.parseCapabilityCommand(future)
return resp, nil
}
func (c *Client) parseCapabilityCommand(future chan []byte) chan CapabilityResponse {
resp := make(chan CapabilityResponse, 1)
go func() {
data := <-future
var response = CapabilityResponse{
PinToModeToResolution: []map[pin.Func]uint8{{}},
SupportedPinModes: [][]pin.Func{{}},
}
var pindex = 0
for i := 0; i < len(data); {
if data[i] == CapabilityResponsePinDelimiter {
response.PinToModeToResolution = append(response.PinToModeToResolution, map[pin.Func]uint8{})
response.SupportedPinModes = append(response.SupportedPinModes, []pin.Func{})
i += 1
pindex++
} else {
pinFunc := pinModeToFuncMap[data[i]]
response.PinToModeToResolution[pindex][pinFunc] = data[i+1]
response.SupportedPinModes[pindex] = append(response.SupportedPinModes[pindex], pinFunc)
i += 2
}
}
c.cr = response
resp <- response
close(resp)
}()
return resp
}
func (c *Client) SendAnalogMappingQuery() (chan AnalogMappingResponse, error) {
future, err := c.SendSysEx(SysExAnalogMappingQuery)
if err != nil {
return nil, err
}
resp := c.parseAnalogMappingQuery(future)
return resp, nil
}
func (c *Client) parseAnalogMappingQuery(future chan []byte) chan AnalogMappingResponse {
resp := make(chan AnalogMappingResponse, 1)
go func() {
data := <-future
var response = AnalogMappingResponse{
AnalogPinToDigital: []uint8{},
DigitalPinToAnalog: map[uint8]uint8{},
}
for i := 0; i < len(data); i++ {
if data[i] != CapabilityResponsePinDelimiter {
response.DigitalPinToAnalog[uint8(i)] = uint8(len(response.AnalogPinToDigital))
response.AnalogPinToDigital = append(response.AnalogPinToDigital, uint8(i))
}
}
c.amr = response
resp <- response
close(resp)
}()
return resp
}
func (c *Client) ExtendedReportAnalogPin(p uint8, value int) error {
if value > 0xFFFFFFFFFFFFFF {
return fmt.Errorf("%w: 0x0 - 0xFFFFFFFFFFFFFF", ErrValueOutOfRange)
}
_, err := c.SendSysEx(SysExExtendedAnalog, p, uint8(value), uint8(value>>7), uint8(value>>14))
if err != nil {
return err
}
return nil
}
func (c *Client) PinStateQuery(p uint8) (chan PinStateResponse, error) {
future, err := c.SendSysEx(SysExPinStateQuery, p)
if err != nil {
return nil, err
}
resp := c.parsePinStateQuery(future)
return resp, nil
}
func (c *Client) parsePinStateQuery(future chan []byte) chan PinStateResponse {
resp := make(chan PinStateResponse, 1)
go func() {
data := <-future
var ps = PinStateResponse{
Pin: data[0],
Mode: pinModeToFuncMap[data[1]],
State: 0,
}
for i, b := range data[2:] {
ps.State |= int(b << (i * 7))
}
resp <- ps
close(resp)
}()
return resp
}
func (c *Client) ReportFirmware() (chan FirmwareReport, error) {
future, err := c.SendSysEx(SysExReportFirmware)
if err != nil {
return nil, err
}
resp := c.parseReportFirmware(future)
return resp, nil
}
func (c *Client) parseReportFirmware(future chan []byte) chan FirmwareReport {
resp := make(chan FirmwareReport, 1)
go func() {
data := <-future
var rc = FirmwareReport{
Major: data[0],
Minor: data[1],
Name: data[2:],
}
resp <- rc
close(resp)
}()
return resp
}
func (c *Client) SetAnalogPinReporting(analogPin uint8, report bool) error {
v := byte(0)
if report {
v = 1
}
return c.write([]byte{byte(ReportAnalogPin) | (analogPin & 0xF), v}, nil)
}
func (c *Client) SetDigitalPinReporting(p uint8, report bool) error {
return c.SetDigitalPortReporting(p%8, report)
}
func (c *Client) SetDigitalPortReporting(port uint8, report bool) error {
v := byte(0)
if report {
v = 1
}
return c.write([]byte{byte(ReportDigitalPort) | (port & 0xF), v}, nil)
}
func (c *Client) SetSamplingInterval(ms uint16) error {
if ms > MaxUInt16 {
return fmt.Errorf("%w: 0x0 - 0x%X", ErrValueOutOfRange, MaxUInt16)
}
return c.write([]byte{byte(SysExSamplingInterval), byte(ms), byte(ms >> 7)}, nil)
}
// This function only supports 7-bit I2C addresses
func (c *Client) WriteI2CData(address uint8, restart bool, data []uint8) error {
if !c.i2cStarted {
return ErrI2CNotEnabled
}
byte2 := byte(I2CModeWrite)
if restart {
byte2 &= I2CRestartTransmission
}
payload := append([]byte{address, byte2}, ByteSliceToTwoByteRepresentation(data)...)
_, err := c.SendSysEx(SysExI2CRequest, payload...)
return err
}
// This function only supports 7-bit I2C addresses
func (c *Client) ReadI2CData(address uint8, restart bool, length uint16) error {
if !c.i2cStarted {
return ErrI2CNotEnabled
}
if length > MaxUInt16 {
return fmt.Errorf("%w: 0x0 - 0xFFFFFFFFFFFFFF", ErrValueOutOfRange)
}
byte2 := byte(I2CModeRead)
if restart {
byte2 &= I2CRestartTransmission
}
lLSB, lMSB := WordToTwoByte(length)
_, err := c.SendSysEx(SysExI2CRequest, address, byte2, lLSB, lMSB)
return err
}
// This function only supports 7-bit I2C addresses
func (c *Client) ReadI2CRegister(address uint8, restart bool, register uint8, length uint16) error {
if !c.i2cStarted {
return ErrI2CNotEnabled
}
if length > MaxUInt16 {
return fmt.Errorf("%w: 0x0 - 0xFFFFFFFFFFFFFF", ErrValueOutOfRange)
}
byte2 := byte(I2CModeRead)
if restart {
byte2 &= I2CRestartTransmission
}
rLSB, rMSB := ByteToTwoByte(register)
lLSB, lMSB := WordToTwoByte(length)
_, err := c.SendSysEx(SysExI2CRequest, address, byte2, rLSB, rMSB, lLSB, lMSB)
return err
}
func (c *Client) SendI2CConfig(delayMicroseconds uint8) error {
micLSB, micMSB := ByteToTwoByte(delayMicroseconds)
_, err := c.SendSysEx(SysExI2CConfig, micLSB, micMSB)
if err != nil {
return err
}
c.i2cStarted = true
return nil
}
func (c *Client) releaseI2CAddressListener(addr uint8) {
c.i2cMU.Lock()
defer c.i2cMU.Unlock()
delete(c.i2cListeners, addr)
}
// This function only supports 7-bit I2C addresses
func (c *Client) SetI2CAddressListener(addr uint8, ch chan I2CPacket) (release func(), err error) {
c.i2cMU.Lock()
defer c.i2cMU.Unlock()
if c.i2cListeners[addr] != nil {
return nil, ErrI2CAddressListenerNotReleased
}
c.i2cListeners[addr] = ch
return func() { c.releaseI2CAddressListener(addr) }, nil
}
func (c *Client) releaseAnalogIOMessageListener(p uint8) {
c.analogPinMU.Lock()
defer c.analogPinMU.Unlock()
delete(c.analogIOMessageChannels, p)
}
func (c *Client) SetAnalogIOMessageListener(p uint8, ch chan uint16) (release func(), err error) {
c.analogPinMU.Lock()
defer c.analogPinMU.Unlock()
if c.analogIOMessageChannels[p] != nil {
return nil, ErrPinListenerNotReleased
}
c.analogIOMessageChannels[p] = ch
return func() { c.releaseAnalogIOMessageListener(p) }, nil
}
func (c *Client) releaseDigitalIOMessageListener(p uint8) {
c.digitalPinMU.Lock()
defer c.digitalPinMU.Unlock()
delete(c.digitalIOMessageChannels, p)
}
func (c *Client) SetDigitalIOMessageListener(p uint8, ch chan gpio.Level) (release func(), err error) {
c.digitalPinMU.Lock()
defer c.digitalPinMU.Unlock()
if c.digitalIOMessageChannels[p] != nil {
return nil, ErrPinListenerNotReleased
}
c.digitalIOMessageChannels[p] = ch
return func() { c.releaseDigitalIOMessageListener(p) }, nil
}
func (c *Client) releaseOneWireListener(p uint8) {
c.onewireMU.Lock()
defer c.onewireMU.Unlock()
delete(c.onewireListeners, p)
}
func (c *Client) SetOneWireListener(p uint8, ch chan []byte) (release func(), err error) {
c.onewireMU.Lock()
defer c.onewireMU.Unlock()
if c.onewireListeners[p] != nil {
return nil, ErrPinListenerNotReleased
}
c.onewireListeners[p] = ch
return func() { c.releaseOneWireListener(p) }, nil
}
func (c *Client) OpenOneWireBus(p uint8) (bus onewire.BusCloser, err error) {
c.onewireMU.Lock()
defer c.onewireMU.Unlock()
if c.onewireListeners[p] != nil {
return nil, ErrPinListenerNotReleased
}
// Need to run configure or firmata will not initialize.
if _, err := c.SendSysEx(SysExOneWireData, byte(OneWireInstructionConfigure), p, 0x00); err != nil {
return nil, err
}
return newOneWireBus(c, newPin(c, p), func() error {
c.releaseOneWireListener(p)
return nil
}), nil
}
func (c *Client) SendAnalogIOMessage(p uint8, value uint16) error {
if p > 0xF {
return ErrAnalogIOMessagePinOutOfRange
}
lsb, msb := WordToTwoByte(value)
return c.write([]byte{byte(AnalogIOMessage) | p, lsb, msb}, nil)
}
func (c *Client) GetPinName(p uint8) string {
if v, ok := c.amr.DigitalPinToAnalog[p]; ok {
return fmt.Sprintf("A%d", v)
}
return fmt.Sprintf("%d", p)
}
func (c *Client) GetPinFunctions(p uint8) []pin.Func {
return c.cr.SupportedPinModes[int(p)]
}
func (c *Client) OpenI2CBus() (i2c.Bus, error) {
return newI2CBus(c)
}