├── .gitignore ├── Bus.go ├── Frame.go ├── LICENSE ├── README.md ├── Transceiver.go ├── replays └── bootup.bin └── utils ├── BusProbe.go └── DeviceProbe.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | BusProbe 3 | -------------------------------------------------------------------------------- /Bus.go: -------------------------------------------------------------------------------- 1 | package gofinity 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | log "github.com/Sirupsen/logrus" 7 | ) 8 | 9 | // Defines a callback for Bus Probing. 10 | type OnFrameReceived func(*Frame) 11 | 12 | // Constants for Bus.status 13 | const ( 14 | READY = uint8(0x00) 15 | RUNNING = uint8(0x01) 16 | STOPPING = uint8(0x02) 17 | INVALID = uint8(0x03) 18 | ) 19 | 20 | // Bus defines frame-based interactions atop a Transceiver 21 | type Bus struct { 22 | waitGroup sync.WaitGroup 23 | transceiver Transceiver 24 | probes []OnFrameReceived 25 | status uint8 26 | } 27 | 28 | // Constructs a new Bus for a BusTransciever 29 | func NewBus(transceiver Transceiver) (*Bus) { 30 | busNode := &Bus{ 31 | transceiver: transceiver, 32 | probes: []OnFrameReceived{}, 33 | waitGroup: sync.WaitGroup{}, 34 | status: READY, 35 | } 36 | 37 | return busNode 38 | } 39 | 40 | func (bus *Bus) Probe(received OnFrameReceived) { 41 | bus.probes = append(bus.probes, received) 42 | } 43 | 44 | // Internal read loop for reading Frames from the transceiver. 45 | func (bus *Bus) readLoop() { 46 | log.Info("Starting Bus.readLoop()") 47 | defer log.Info("Bus.readLoop() finished") 48 | defer bus.waitGroup.Done() 49 | 50 | frameBuf := []byte{} 51 | readBuf := make([]byte, 1024) 52 | 53 | // If the transceiver is no longer valid, bail. 54 | for bus.status == RUNNING && bus.transceiver.Valid() { 55 | // If the transceiver isn't open, reset the framebuffer and (re)open. 56 | if !bus.transceiver.IsOpen() { 57 | frameBuf = []byte{} 58 | bus.transceiver.Open() 59 | } 60 | 61 | // Try to read some bytes. 62 | n, readErr := bus.transceiver.Read(readBuf) 63 | 64 | // Append to the frame buffer the bytes we just read. 65 | frameBuf = append(frameBuf, readBuf[:n]...) 66 | 67 | for { 68 | // Make sure we have at least a full header. 69 | if len(frameBuf) < 10 { 70 | break 71 | } 72 | 73 | // Byte 5 of valid frames tell us how long the frame is, plus header length. 74 | frameLength := int(frameBuf[4]) + 10 75 | if len(frameBuf) < frameLength { 76 | break; 77 | } 78 | 79 | frameSlice := frameBuf[:frameLength] 80 | 81 | frame, err := NewFrame(frameSlice) 82 | if err == nil { 83 | for _, probe := range bus.probes { 84 | probe(frame) 85 | } 86 | 87 | // This portion, (0 - length), handled. 88 | // Slice (advance) the buffer. 89 | frameBuf = frameBuf[:copy(frameBuf, frameBuf[frameLength:])] 90 | } else { 91 | // Corrupt Message, or not quite a frame yet. 92 | // Advance one byte, try again. 93 | frameBuf = frameBuf[:copy(frameBuf, frameBuf[1:])] 94 | } 95 | } 96 | 97 | if readErr != nil { 98 | log.Warn("Erorr reading : ", readErr) 99 | bus.transceiver.Close() 100 | } 101 | } 102 | 103 | if !bus.transceiver.Valid() { 104 | bus.status = INVALID 105 | log.Info("Transceiver no longer valid for reading.") 106 | } 107 | } 108 | 109 | // Starts the Bus's I/O loops. 110 | func (bus *Bus) Start() error { 111 | if bus.status != READY { 112 | return errors.New("Bus not in 'READY' state.") 113 | } 114 | bus.status = RUNNING 115 | 116 | bus.waitGroup.Add(1) 117 | go bus.readLoop() 118 | 119 | return nil 120 | } 121 | 122 | // Stops the Bus's I/O loops. 123 | // Blocks until all threads running for I/O terminate. 124 | func (bus *Bus) Shutdown() error { 125 | if bus.status != RUNNING || bus.status != INVALID { 126 | return errors.New("Bus not 'RUNNING' or 'INVALID'.") 127 | } 128 | bus.status = STOPPING 129 | bus.waitGroup.Wait() 130 | bus.status = READY 131 | 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /Frame.go: -------------------------------------------------------------------------------- 1 | package gofinity 2 | 3 | import ( 4 | "github.com/npat-efault/crc16" 5 | "errors" 6 | "encoding/binary" 7 | log "github.com/Sirupsen/logrus" 8 | "fmt" 9 | "strconv" 10 | ) 11 | 12 | const ACK = 0x06 13 | const READ_REQUEST = 0x0b 14 | const WRITE_REQUEST = 0x0c 15 | const ERROR = 0x15 16 | 17 | var Operations = map[uint8]string{ 18 | ACK: "ACK", 19 | READ_REQUEST: "READ_REQUEST", 20 | WRITE_REQUEST: "WRITE_REQUEST", 21 | ERROR: "ERROR", 22 | } 23 | 24 | // A Frame Header 25 | type Header struct { 26 | Destination uint16 27 | Source uint16 28 | Length uint8 29 | reserved1 uint8 // Not sure what these two bytes are yet 30 | reserved2 uint8 31 | Operation uint8 32 | } 33 | 34 | // A Communication Frame (message) 35 | type Frame struct { 36 | Header Header 37 | payload []byte 38 | checksum uint16 39 | } 40 | 41 | // Decodes and creates a new Frame from the given buffer. 42 | func NewFrame(buf []byte) (*Frame, error) { 43 | empty := true 44 | for _, c := range buf { 45 | if c != 0 { 46 | empty = false 47 | break 48 | } 49 | } 50 | 51 | if empty { 52 | return nil, errors.New("No Frame Content") 53 | } 54 | 55 | // End of the buffer is a 2 byte checksum. 56 | headerDataLength := len(buf) - 2 // Length of Header + Data - Checksum 57 | 58 | // Calculate a checksum from the bytes we've received. 59 | rxChecksum := crc16.Checksum(crcConfig, buf[:headerDataLength]) 60 | 61 | // Read the checksum from the buffer 62 | txChecksum := binary.LittleEndian.Uint16(buf[headerDataLength:]) 63 | 64 | if rxChecksum != txChecksum { 65 | log.Error(fmt.Sprintf("Checksum Mismatch: %x != %x", rxChecksum, txChecksum)) 66 | return nil, errors.New("Frame Checksum mismatch") 67 | } 68 | 69 | // Checksum matches. Construct the frame. 70 | return &Frame{ 71 | Header: Header{ 72 | Destination: binary.LittleEndian.Uint16(buf[0:2]), 73 | Source: binary.LittleEndian.Uint16(buf[2:4]), 74 | Length: buf[4], // uint8 75 | reserved1: buf[5], // Not sure what this byte and the next are. 76 | reserved2: buf[6], 77 | Operation: buf[7], // uint8 78 | }, 79 | payload: buf[8:headerDataLength], 80 | checksum: txChecksum, // uint16 81 | }, nil 82 | } 83 | 84 | func NewProbeDeviceFrame(source uint16, destination uint16, exportIdx uint16, offset uint8) (*Frame) { 85 | probeFrame := Frame{ 86 | Header: Header{ 87 | Destination: destination, 88 | Source: source, 89 | Operation: READ_REQUEST, 90 | }, 91 | } 92 | 93 | // Put together the payload. 94 | probeFrame.payload = make([]byte, 3) 95 | 96 | // Export index, first entry. 97 | binary.BigEndian.PutUint16(probeFrame.payload, exportIdx) 98 | probeFrame.payload[2] = offset 99 | 100 | return &probeFrame 101 | } 102 | 103 | func (header *Header) String() string { 104 | return fmt.Sprintf("%4x -> %4x [%3d] : %s %s : %14s", 105 | header.Source, header.Destination, header.Length, 106 | strconv.FormatUint(uint64(header.reserved1), 2), 107 | strconv.FormatUint(uint64(header.reserved2), 2), 108 | Operations[header.Operation]) 109 | 110 | } 111 | 112 | func (frame *Frame) Encode() ([]byte) { 113 | frame.Header.Length = uint8(len(frame.payload)) 114 | 115 | // Create a buffer big enough. 116 | buf := make([]byte, frame.Header.Length+8) // Header length in bytes. 117 | 118 | binary.LittleEndian.PutUint16(buf[0:2], frame.Header.Destination) 119 | binary.LittleEndian.PutUint16(buf[2:4], frame.Header.Source) 120 | buf[4] = frame.Header.Length 121 | buf[5] = 0x00 122 | buf[6] = 0x00 123 | buf[7] = frame.Header.Operation 124 | copy(buf[8:], frame.payload) 125 | 126 | // Calculate the CRC. 127 | crc := crc16.Checksum(crcConfig, buf) 128 | 129 | // Make a new buffer big enough to hold the output + crc 130 | outbuf := make([]byte, len(buf)+2) 131 | copy(outbuf, buf) 132 | binary.LittleEndian.PutUint16(outbuf[len(buf):], crc) 133 | 134 | return outbuf 135 | } 136 | 137 | func (frame *Frame) String() string { 138 | return fmt.Sprintf("%s : %s", frame.Header.String(), frame.Payload()) 139 | } 140 | 141 | func (frame *Frame) Payload() string { 142 | if len(frame.payload) == 1 && frame.payload[0] == 0x00 { 143 | return "" 144 | } else { 145 | expStruct := binary.BigEndian.Uint16(frame.payload[0:2]) // Index of array of pointers, pointing to memory addresses of structures containing data type, name, and count of similar in-memory structs. 146 | expIdx := frame.payload[2] // Resolve Pointer to struct. Now, what index are we interested in? 147 | 148 | log.Debug(fmt.Sprintf("%x", frame.payload)) 149 | 150 | return fmt.Sprintf("exports[%d].data[%d]", expStruct, expIdx) 151 | } 152 | } 153 | 154 | // Global Checksum configuration. 155 | var crcConfig = &crc16.Conf{ 156 | Poly: 0x8005, BitRev: true, 157 | IniVal: 0x0, FinVal: 0x0, 158 | BigEnd: false, 159 | } 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bryan Varner 4 | Copyright (c) 2016 Andrew Danforth 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is 'gofinity' 2 | Gofinity is a library for interacting with the Carrier Infinity / Bryant Evolution communicating residential HVAC systems. 3 | 4 | This library exports 5 | * Bus communication snooping / decoding 6 | * Bus device state tracking 7 | * Tools to help support reverse engineering device communications / internals 8 | 9 | The code in this repository can loosely trace it's heritage back to 10 | [Andrew Danforth's Infinitive project](https://github.com/acd/infinitive) which aims to emulate a SAM device. 11 | 12 | ## History 13 | This project has evolved out of my desire to data-log everything on my HVAC system. 14 | I'm particularly interested in: 15 | * logging all temperature sensor data (my outdoor unit is always wrong when it's a sunny day) and correlating it with 16 | 3rd party metrics / measurements (weather stations). 17 | * logging BTUs produced / moved by the system. 18 | * logging energy consumption. 19 | * Calculating the effective R-Value of my building envelope. 20 | 21 | Longer-term, I want to be able to: 22 | * Control my system like a SAM. (but have it actually work with Bryant systems) 23 | * Segment the RS-485 network to interdict system commands to subordinate devices. 24 | * Create a zoning control module. 25 | * Build an 'economizer' module that transparently works in conjunction with the heat-pump. 26 | 27 | I have a fancy hybrid heat pump & variable throttled furnace, and my utility company costs fluctuate enough that given 28 | specific environmental factors, it may be less expensive to use gas or electricity (heat pump) for heating. I also live 29 | in a very temperate zone of the north american continent, where for roughly 8 months of the year the outdoor ambient air 30 | could be used for cooling with a fan rather than running the compressor on the heat pump. 31 | 32 | My night-time 'set back' with the thermostat in 'auto' mode results in my heat pump compressor activating, when I could 33 | economize with just ambient air from outside. 34 | 35 | The HVAC company solution to outdoor air involves a heat exchanger to try to equalize incoming ambient air temperatures 36 | to the same temperature as the conditioned space. Using this system to try and 'cool' a home would result in the fan 37 | having to blow for considerably longer than otherwise necessary if conditioned air were simply exhausted and exchanged 38 | for ambient outside air. 39 | 40 | This all came about, as a consequence of talking with my HVAC tech (whom I also know outside of his professional life) 41 | about my economizer idea. Turns out, there are commercial products out there that do this kind of thing, but they're 42 | pretty expensive and don't do a great job of integrating with the Carrier / Bryant communicating systems. My friend did 43 | the opposite of trying to talk me out of it. 44 | 45 | Challenge Accepted. 46 | 47 | 48 | # Protocol / Device Information 49 | 50 | ## Physical ABCD Bus 51 | **Half Duplex RS-485 + 24VAC = ABCD** 52 | 53 | Inside Carrier Infinity / Bryant Evolution systems there's a set of screw terminals labeled, "ABCD". These are the very 54 | wires which connect the thermostat, air handler, and any outdoor units (heat pump, air conditioner). 55 | 56 | The A & B terminals carry RS-485 serial data at 38400, 8n1. The C & D Terminals provide 24VAC. 57 | 58 | ## RS-485 Hardware 59 | You'll need some way to communicate with the RS-485 bus of the ABCD connectors on your HVAC system. 60 | Instead of dealing with the shortcomings of common USB transceivers and looking more toward my final goals of embedded 61 | custom hardware & software, I designed and built [pi485](https://github.com/bvarner/pi485), a TTL Serial to RS-485 62 | transceiver for Arduino and Raspberry Pi type devices. These are not difficult to make, and a full BOM will cost about 63 | $15 USD (2017 prices) in single quantities. You could probably do it for less than that if you have a bunch of things in 64 | your 'junk drawer'. 65 | 66 | Or, you could go the RS-485 -> USB Converter route. Any of these can work. 67 | 68 | Beware hardware that doesn't have a way to disable termination resistors, as that 69 | will cause issues with your ABCD bus. Also, devices which don't properly bias the AB lines with the DC supply voltage 70 | and 'ground', may have issues as well. These issues (and I wanted to use the UART rather than USB) were why I created 71 | [pi485](https://github.com/bvarner/pi485). 72 | 73 | ## Protocol Basics 74 | 75 | 76 | ### Bootup Sequence 77 | 78 | 79 | ### 80 | 81 | 82 | -------------------------------------------------------------------------------- /Transceiver.go: -------------------------------------------------------------------------------- 1 | package gofinity 2 | 3 | import ( 4 | "io" 5 | "github.com/tarm/serial" 6 | "os" 7 | "time" 8 | log "github.com/Sirupsen/logrus" 9 | ) 10 | 11 | // Transceiver abstracts a ReadWriteCloser and the functions expected by Bus for opening / closing streams. 12 | type Transceiver interface { 13 | io.ReadWriteCloser 14 | Open() error 15 | IsOpen() bool 16 | Valid() bool 17 | } 18 | 19 | // SerialTransceiver is a Transceiver that operations on serial ports. Surprise! 20 | type SerialTransceiver struct { 21 | device string 22 | port *serial.Port 23 | } 24 | 25 | func NewSerialTransceiver(device string) (*SerialTransceiver) { 26 | return &SerialTransceiver{device: device} 27 | } 28 | 29 | func (st *SerialTransceiver) Read(p []byte) (n int, err error) { 30 | return st.port.Read(p) 31 | } 32 | 33 | func (st *SerialTransceiver) Write(p []byte) (n int, err error) { 34 | return st.port.Write(p) 35 | } 36 | 37 | func (st *SerialTransceiver) Close() error { 38 | err := st.port.Close() 39 | st.port = nil 40 | return err 41 | } 42 | 43 | func (st *SerialTransceiver) Open() error { 44 | var err error 45 | config := &serial.Config{Name: st.device, Baud: 38400, ReadTimeout: (time.Second * 30)} 46 | st.port, err = serial.OpenPort(config) 47 | if err != nil { 48 | st.port = nil 49 | } 50 | return err 51 | } 52 | 53 | func (st *SerialTransceiver) IsOpen() bool { 54 | return st.port != nil 55 | } 56 | 57 | func (st *SerialTransceiver) Valid() bool { 58 | return true 59 | } 60 | 61 | // FileReplayer is a Transceiver that turns writes into 'no-ops', but allows for probing previously recorded bus logs. 62 | // if a FileReplayer hits an EOF, it's considered no longer valid. 63 | type FileReplayer struct { 64 | fileName string 65 | file *os.File 66 | atEOF bool 67 | } 68 | 69 | func NewFileBusReplayer(file string) (*FileReplayer) { 70 | return &FileReplayer{fileName: file, file: nil, atEOF: false} 71 | } 72 | 73 | func (fb *FileReplayer) Read(p []byte) (n int, err error) { 74 | n, err = fb.file.Read(p) 75 | if err == io.EOF { 76 | fb.atEOF = true 77 | } 78 | return n,err 79 | } 80 | 81 | func (fb *FileReplayer) Write(p []byte) (n int, err error) { 82 | // Act like we wrote the whole thing 83 | return len(p), nil 84 | } 85 | 86 | func (fb *FileReplayer) Close() error { 87 | err := fb.file.Close() 88 | fb.file = nil 89 | return err 90 | } 91 | 92 | func (fb *FileReplayer) Open() error { 93 | var err error 94 | log.Debug("Attempting to Open: ", fb.fileName) 95 | fb.file, err = os.Open(fb.fileName) 96 | if err != nil { 97 | fb.file = nil 98 | } 99 | return err 100 | } 101 | 102 | func (fb *FileReplayer) IsOpen() bool { 103 | return fb.file != nil 104 | } 105 | 106 | func (fb *FileReplayer) Valid() bool { 107 | return !fb.atEOF 108 | } 109 | -------------------------------------------------------------------------------- /replays/bootup.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bvarner/gofinity/5604cdad5133421edb5d30b962f574f68818d5ad/replays/bootup.bin -------------------------------------------------------------------------------- /utils/BusProbe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/bvarner/gofinity" 6 | "time" 7 | log "github.com/Sirupsen/logrus" 8 | ) 9 | 10 | func main() { 11 | log.SetLevel(log.DebugLevel) 12 | log.Info("Starting up BusProbe.") 13 | 14 | serialPort := flag.String("s", "", "path to serial port device") 15 | replayFile := flag.String("f", "", "binary capture file to replay") 16 | 17 | flag.Parse() 18 | 19 | var transceiver gofinity.Transceiver = nil 20 | 21 | if len(*serialPort) != 0 { 22 | transceiver = gofinity.NewSerialTransceiver(*serialPort) 23 | } 24 | if len(*replayFile) != 0 { 25 | transceiver = gofinity.NewFileBusReplayer(*replayFile) 26 | } 27 | 28 | if transceiver == nil { 29 | defer flag.PrintDefaults() 30 | log.Fatal("You must specify either -s (serial device) or -f (replay flie)") 31 | } 32 | 33 | err := transceiver.Open() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | bus := gofinity.NewBus(transceiver) 39 | bus.Probe(func(frame *gofinity.Frame) { 40 | log.Info(frame) 41 | }) 42 | 43 | err = bus.Start() 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | defer bus.Shutdown() 48 | 49 | for transceiver.IsOpen() { 50 | log.Debug("Sleeping 5 seconds for transceiver isOpen test...") 51 | time.Sleep(time.Second * 5) 52 | } 53 | 54 | log.Info("Transceiver closed") 55 | } 56 | -------------------------------------------------------------------------------- /utils/DeviceProbe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/bvarner/gofinity" 6 | log "github.com/Sirupsen/logrus" 7 | "time" 8 | "fmt" 9 | "os" 10 | ) 11 | 12 | // This is a really nasty, mean little tool, that disrespects "proper" device interaction on the bus by writing whenever 13 | // it darn well wants to (which is really not the way this "should" be working, methinks) 14 | // 15 | // I have yet to record enough logs including device adding to the network (i.e. I haven't power-cycled my heat pump 16 | // while logging everything yet) 17 | // 18 | // But at least this exercises some basic frame encoding and writing, and it seems to work reasonably well. 19 | func main() { 20 | log.SetLevel(log.DebugLevel) 21 | 22 | // I'm using terminology that's not "in line" with what everyone else is using. 23 | // Firstly, I reject the term 'table' on principle. 24 | // My current hypothesis is that these are pointers to pointers to structs (well, that's the simplified version) 25 | // or at the very least array indexes inside an array -- likely arrays of pointers to structs. Yep. 26 | // I'd kill for a copy of the header file that defines the standards for this protocol. 27 | // I also bet there's a few engineers at Carrier that laugh about the current attempts to decipher this stuff. 28 | serialPort := flag.String("s", "", "path to serial port device") 29 | devAddr := flag.Uint("a", 0x121, "Address of this device") 30 | probeAddr := flag.Uint("p", 0xf1f1, "Address of the device to probe") 31 | exportIdx := flag.Uint( "i", 0x0001, "Address (index) of exported API to probe") 32 | offset := flag.Uint("o", 0x01, "Offset within the API") 33 | 34 | flag.Parse() 35 | 36 | var transceiver gofinity.Transceiver = nil 37 | 38 | // We're only going to let this tool work on real serial connections. 39 | if len(*serialPort) != 0 { 40 | transceiver = gofinity.NewSerialTransceiver(*serialPort) 41 | } 42 | 43 | if transceiver == nil { 44 | defer flag.PrintDefaults() 45 | log.Fatal("You must specify a -s (serial device) for device probing.") 46 | } 47 | 48 | // Open the transciever (serial port) 49 | err := transceiver.Open() 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | // Create a new Bus instance on the transceiver, and add a probe for incoming Frames. 55 | bus := gofinity.NewBus(transceiver) 56 | bus.Probe(func(frame *gofinity.Frame) { 57 | // If it's sent to the destination we're listening on... 58 | if frame.Header.Destination == uint16(*devAddr) { 59 | log.Info(frame) 60 | bus.Shutdown() 61 | } 62 | }) 63 | 64 | // Start up the bus interaction. 65 | err = bus.Start() 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | defer bus.Shutdown() 70 | 71 | 72 | // TODO: In the future, this would be queued for a write in the bus, where we'd be issuing our outgoing frames 73 | // after the coordinator polls us. 74 | // Or, if we're a coordinator, after a few hundred ms of inactivity on the bus. 75 | // For now, create a frame and slam that sucker into the bus, consequences be darned. 76 | probeFrame := gofinity.NewProbeDeviceFrame(uint16(*devAddr), uint16(*probeAddr), uint16(*exportIdx), uint8(*offset)) 77 | toSend := probeFrame.Encode() 78 | 79 | if transceiver.IsOpen() { 80 | log.Debug(fmt.Sprintf("Sending %s", probeFrame.String())) 81 | log.Debug("Writing ", len(toSend), " bytes") 82 | n, error := transceiver.Write(toSend) 83 | 84 | log.Info("Wrote ", n, " bytes with error: ", error) 85 | } 86 | 87 | 88 | // FWIW, we don't always get a response back in the timeframe you want it in. 89 | for transceiver.IsOpen() { 90 | log.Debug("Sleeping 5 seconds for transceiver isOpen test...") 91 | time.Sleep(time.Second * 5) 92 | } 93 | 94 | log.Info("Transceiver closed") 95 | 96 | } 97 | --------------------------------------------------------------------------------