package main import ( "bufio" "bytes" "encoding/binary" "encoding/hex" "errors" "fmt" "github.com/TwiN/go-color" "github.com/albenik/go-serial/v2" "github.com/augustoroman/hexdump" "log" "math" "math/bits" "net" "os" "regexp" "runtime" "strconv" "strings" "sync" "time" "github.com/howeyc/crc16" ) var SCSversion = map[string]string{ "A": "PTC-II", "B": "PTC-IIpro", "C": "PTC-IIe", "D": "PTC-IIex", "E": "PTC-IIusb", "F": "PTC-IInet", "H": "DR-7800", "I": "DR-7400", "K": "DR-7000", "L": "PTC-IIIusb", "T": "PTC-IIItrx", } type pflags struct { exit bool stopmodem bool //disconnected chan struct{} connected chan struct{} closeWaiting chan struct{} } type pmux struct { device sync.Mutex write sync.Mutex read sync.Mutex close sync.Mutex bufLen sync.Mutex } type Modem struct { devicePath string localAddr string remoteAddr string state State device *serial.Port tcpdevice net.Conn mux pmux wg sync.WaitGroup flags pflags goodChunks int packetcounter bool chanbusy bool cmdlineinit string initfile string closeOnce sync.Once } const ( SerialTimeout = 1 PactorChannel = 4 NMEAChannel = 249 TRXControlChannel = 253 MaxSendData = 255 MaxFrameNotTX = 2 ) // ToPactor states const ( Closed State = iota Ready Connecting Conntected ) type State uint8 //var debugMux sync.Mutex func debugEnabled() int { if value, ok := os.LookupEnv("PACTOR_DEBUG"); ok { level, err := strconv.Atoi(value) if err == nil { return level } } return 0 } func writeDebug(message string, level int) { if debugEnabled() >= level { _, file, no, ok := runtime.Caller(1) if ok { log.Println(file + "#" + strconv.Itoa(no) + ": " + message) } else { log.Println(message) } } return } // OpenModem Initialise the pactor modem and all variables. Switch the modem into hostmode. // // Will abort if modem reports failed link setup, Close() is called or timeout // has occured (90 seconds) func OpenModem(path string, baudRate int, myCall string, initfile string, cmdlineinit string) (p *Modem, err error) { writeDebug("Trying to open "+path+" with "+strconv.Itoa(baudRate)+" baud.", 0) p = &Modem{ // Initialise variables devicePath: path, localAddr: myCall, remoteAddr: "", state: Closed, device: nil, flags: pflags{ exit: false, stopmodem: false, connected: make(chan struct{}, 1), closeWaiting: make(chan struct{}, 1)}, goodChunks: 0, // cmdBuf: make(chan string, 0), cmdlineinit: cmdlineinit, initfile: initfile, } writeDebug("Initialising pactor modem", 1) if strings.HasPrefix(p.devicePath, "tcp://") { if p.tcpdevice, err = net.Dial("tcp", strings.Replace(p.devicePath, "tcp://", "", 1)); err != nil { writeDebug(err.Error(), 0) return nil, err } } else { // Check if serial device exists if err := p.checkSerialDevice(); err != nil { writeDebug(err.Error(), 0) return nil, err } //Setup serial device if p.device, err = serial.Open(p.devicePath, serial.WithBaudrate(baudRate), serial.WithReadTimeout(SerialTimeout)); err != nil { writeDebug(err.Error(), 1) time.Sleep(3 * time.Second) return nil, err } } err = p.init() if err != nil { return nil, err } // run modem thread time.Sleep(3 * time.Second) // give the device a moment to settle, so no commands get lost return p, nil } func (p *Modem) setState(state State) { writeDebug("Setting state to: "+strconv.FormatUint(uint64(state), 10), 1) p.state = state } func (p *Modem) IsClosed() bool { if p == nil { return true } return p.state == Closed } func (p *Modem) init() (err error) { writeDebug("Entering PACTOR init", 1) // Get modem out of CRC hostmode if it is still in it while starting. //p.stophostmode() //p._write(string([]byte{0xaa, 0xaa, 0x00, 0x01, 0x05, 0x4a, 0x48, 0x4f, 0x53, 0x54, 0x30, 0xfb, 0x3d})) //time.Sleep(100 * time.Millisecond) //_, _, _ = p._read(100) if _, _, err = p.writeAndGetResponse("", -1, false, 10240); err != nil { return err } if _, _, err = p.writeAndGetResponse("", -1, false, 10240); err != nil { return err } time.Sleep(time.Second) // Make sure, modem is in main menu. Will respose with "ERROR:" when already in it -> Just discard answer! _, ans, err := p.writeAndGetResponse("Quit", -1, false, 1024) if err != nil { return err } if len(ans) < 2 { return errors.New("modem does not react to Quit command. Please re-power your modem") } // Check if we can determine the modem's type _, ver, err := p.writeAndGetResponse("ver ##", -1, false, 1024) if err != nil { return err } re, err := regexp.Compile(`\w#1`) if err != nil { return errors.New("Cannot read SCS modem version string, did you configure the correct serial device? Error: " + err.Error()) } version := strings.ReplaceAll(string(re.Find([]byte(ver))), "#1", "") modem, exists := SCSversion[version] if !exists { return errors.New("Found a modem type: " + ver + " which this driver doesn't support. Please contact the author.") } writeDebug("Found a "+modem+" modem at "+p.devicePath, 0) s.DeviceType = modem writeDebug("Running init commands", 1) ct := time.Now() commands := []string{"DD", "RESTART", "MYcall " + p.localAddr, "PTCH " + strconv.Itoa(PactorChannel), "MAXE 35", "REM 0", "CHOB 0", "PD 1", "ADDLF 0", "ARX 0", "BELL 0", "BC 0", "BKCHR 25", "CMSG 0", "LFIGNORE 0", "LISTEN 0", "MAIL 0", "REMOTE 0", "PDTIMER 5", "STATUS 1", "TONES 4", "MARK 1600", "SPACE 1400", "CWID 0", "CONType 3", "MODE 0", "DATE " + ct.Format("020106"), "TIME " + ct.Format("150405")} //run additional commands provided on the command line with "init" if p.cmdlineinit != "" { for _, command := range strings.Split(strings.TrimSuffix(p.cmdlineinit, "\n"), ";") { commands = append(commands, command) } } for _, cmd := range commands { var res string writeDebug("Sending command to modem: "+cmd, 1) _, res, err = p.writeAndGetResponse(cmd, -1, false, 1024) if err != nil { return err } if strings.Contains(res, "ERROR") { log.Print(`Command "` + cmd + `" not accepted: ` + res) return fmt.Errorf(`Command "` + cmd + `" not accepted: ` + res) } else { l := fmt.Sprintf("%s --> "+color.InGreen("OK\n"), cmd) // print some output log.Println(l) fmt.Println(l) } } // run additional commands stored in the init script file if p.initfile != "" { if err = p.runInitScript(p.initfile); err != nil { return err } } p.flags.stopmodem = false p.setState(Ready) go p.modemThread() //p.flags.exit = false return nil } // Send additional initialisation command stored in the InitScript // // Each command has to be on a new line func (p *Modem) runInitScript(initScript string) error { if _, err := os.Stat(initScript); os.IsNotExist(err) { return fmt.Errorf("ERROR: PTC init script defined but not existent: %s", initScript) } file, err := os.Open(initScript) if err != nil { return err } defer func() { err := file.Close() if err != nil { writeDebug(err.Error(), 0) } }() scanner := bufio.NewScanner(file) for scanner.Scan() { writeDebug("Sending command to modem: "+scanner.Text(), 0) if _, _, err = p.writeAndGetResponse(scanner.Text(), -1, false, 1024); err != nil { return err } } if err := scanner.Err(); err != nil { return err } return nil } func (p *Modem) modemThread() { writeDebug("start modem thread", 1) _, _, _ = p.read(1024) _, _, _ = p.writeAndGetResponse("JHOST4", -1, false, 1024) _, _, _ = p.read(1024) // need to wait a second for WA8DED mode to "settle" time.Sleep(time.Second) const chunkSize = 1024 p.wg.Add(1) for !p.flags.stopmodem { // TX data if p.getNumFramesNotTransmitted() < MaxFrameNotTX { // data := p.getSendData() data, err := s.Data.Data.Dequeue(256) if err == nil { //data := []byte(d.(string)) writeDebug("Write data ("+strconv.Itoa(len(data))+"): "+hex.EncodeToString(data), 2) // TODO: Catch errors! _, _, _ = p.writeAndGetResponse(string(data), PactorChannel, false, chunkSize) time.Sleep(1000 * time.Millisecond) // wait for the data to be filled into the buffer if s.VARAMode { s.Command.Response.Enqueue(fmt.Sprintf("BUFFER %d", s.Data.Data.GetLen())) } } } // TX commands cmd, err := s.Command.Cmd.Dequeue() if err == nil { writeDebug("Write command ("+strconv.Itoa(len(cmd))+"): "+cmd, 1) _, ans, err := p.writeAndGetResponse(cmd, PactorChannel, true, chunkSize) if err != nil { writeDebug("Error when sending Command: "+err.Error(), 0) } if strings.HasPrefix(cmd, "C ") || strings.HasPrefix(cmd, "c ") { // in VARA mode we need to know if the connection is outgoing or incoming so mark outgoing connects p.setState(Connecting) } if len(ans) > 2 { if !s.VARAMode { s.Command.Response.Enqueue(ans[2:]) } } writeDebug("Answer from modem: "+ans, 1) // TODO: Catch errors! } // TX TRX control data trxcmd, err := s.ToTRX.Dequeue(1024) if err == nil { writeDebug("Write TRX command ("+strconv.Itoa(len(trxcmd))+"): "+hexdump.Dump(trxcmd), 1) _, _, err := p.writeAndGetResponse(string(trxcmd), TRXControlChannel, false, chunkSize) if err != nil { writeDebug("Error when sending TRX Command: "+err.Error(), 0) } /*if len(ans) > 2 { s.FromTRX.Enqueue([]byte(ans[2:])) }*/ //s.FromTRX.Enqueue([]byte(ans)) //writeDebug("TRX CMD answer from modem: \n"+hexdump.Dump([]byte(ans)), 1) } // RX var res []byte if channels, err := p.getChannelsWithOutput(); err == nil { for _, c := range channels { _, data, _ := p.writeAndGetResponse("G", c, true, chunkSize) if _, res, err = p.checkResponse(data, c); err != nil { writeDebug("checkResponse returned: "+err.Error(), 1) } else { if res != nil { writeDebug("response: "+string(res)+"\n"+hex.Dump(res), 3) switch c { case PactorChannel: err := s.Data.Response.Enqueue(res) if err != nil { writeDebug(err.Error(), 0) } writeDebug("Written to sendbuf", 3) case 254: //Status update p.chanbusy = int(res[0])&112 == 112 // See PTC-IIIusb manual "STATUS" command writeDebug("PACTOR state: "+strconv.FormatInt(int64(res[0]), 2), 2) case TRXControlChannel: //if !bytes.Equal(res, trxcmd) { err := s.FromTRX.Enqueue(res) if err != nil { writeDebug(err.Error(), 0) } writeDebug("TRX CMD answer:\n"+hexdump.Dump(res), 1) //} default: writeDebug("Channel "+strconv.Itoa(c)+": "+string(res), 1) } } } } } } time.Sleep(100 * time.Millisecond) _, _, err := p.writeAndGetResponse("JHOST0", PactorChannel, true, chunkSize) if err != nil { writeDebug("PACTOR ERROR Could not get modem out of the WA8DED mode: "+err.Error(), 0) } writeDebug("Modemthread is about to end", 1) time.Sleep(100 * time.Millisecond) p.wg.Done() //p.ModemClose() writeDebug("exit modem thread", 1) } func (p *Modem) Close() error { _, file, no, ok := runtime.Caller(1) if ok { writeDebug("PACTOR Close called from "+file+"#"+strconv.Itoa(no), 1) } else { writeDebug("PACTOR Close called", 1) } var err error p.closeOnce.Do(func() { err = p.close() }) return err } func (p *Modem) stophostmode() { writeDebug("Stopping WA8DED hostmode", 0) var ok bool var err error // Send JHOST0 in CRC-enhanced WA8DED mode syntax for ok == false { buff := make([]byte, 100) //var n int _ = p.write(string([]byte{0xaa, 0xaa, 0x00, 0x01, 0x05, 0x4a, 0x48, 0x4f, 0x53, 0x54, 0x30, 0xfb, 0x3d})) time.Sleep(100 * time.Millisecond) for { n, _, err := p.read(100) if err != nil { log.Fatal(err) } if n == 0 { break } } err = p.write("\rRESTART\r") if err != nil { writeDebug(err.Error(), 0) } time.Sleep(1000 * time.Millisecond) for { n, a, err := p.read(100) if err != nil { log.Fatal(err) } if n == 0 { break } copy(buff, a) //fmt.Printf("%v", string(buff[:n])) } ok, err = regexp.Match("cmd:", buff) if err != nil { writeDebug("Error in stophostmode: "+err.Error(), 0) } } } // Wait for transmission to be finished // // BLOCKING until either all frames are transmitted and acknowledged or timeouts // occurs func (p *Modem) waitTransmissionFinish(t time.Duration) error { timeout := time.After(t) tick := time.Tick(500 * time.Millisecond) for { notAck := p.getNumFrameNotAck() notTrans := p.getNumFramesNotTransmitted() select { case <-timeout: if notAck != 0 && notTrans != 0 { return errors.New("Timeout: " + strconv.Itoa(notAck) + "frames not ack and " + strconv.Itoa(notTrans) + " frames not transmitted") } else if notAck != 0 { return errors.New("Timeout: " + strconv.Itoa(notAck) + "frames not ack") } else { return errors.New("Timeout: " + strconv.Itoa(notTrans) + " frames not transmitted") } case <-tick: if notAck == 0 && notTrans == 0 { return nil } } } } // Close current serial connection. Stop all threads and close all channels. func (p *Modem) close() (err error) { _, file, no, ok := runtime.Caller(1) if ok { writeDebug("PACTOR close called from "+file+"#"+strconv.Itoa(no), 1) } else { writeDebug("PACTOR close called", 1) } writeDebug("signal modem thread to stop", 1) p.flags.stopmodem = true writeDebug("waiting for all threads to exit", 1) p.wg.Wait() writeDebug("modem thread stopped", 1) p.stophostmode() p.setState(Closed) writeDebug("PACTOR close() finished", 1) return nil } // Get number of frames not yet transmitted by the modem func (p *Modem) getNumFramesNotTransmitted() int { //return p.channelState.c return 0 } // Get number of frames not acknowledged by the remote station func (p *Modem) getNumFrameNotAck() int { //return p.channelState.d return 0 } // Check if serial device is still available (e.g. still connected) func (p *Modem) checkSerialDevice() (err error) { if _, err := os.Stat(p.devicePath); os.IsNotExist(err) { return fmt.Errorf("ERROR: Device %s does not exist", p.devicePath) } return nil } // Poll channel 255 with "G" command to find what channels have output. // Write them into the channels list. func (p *Modem) getChannelsWithOutput() (channels []int, err error) { //Poll Channel 255 to find what channels have output n, chs, err := p.writeAndGetResponse("G", 255, true, 1024) if err != nil { return nil, err } channels = make([]int, 0) for i, ch := range []byte(chs)[:n-1] { if (i == 0 && ch == 255) || (i == 1 && ch == 1) { continue } channels = append(channels, int(ch)-1) } writeDebug("Channels with output: "+fmt.Sprintf("%#v", channels), 2) return } // Do some checks on the returned data. // // Currently, only connection information (payload) messages are passed on func (p *Modem) checkResponse(resp string, ch int) (n int, data []byte, err error) { if len(resp) < 3 { return 0, nil, fmt.Errorf("no data") } head := []byte(resp[:3]) payload := []byte(resp[3:]) length := int(head[2]) + 1 pl := len(payload) if int(head[0]) != ch { writeDebug("WARNING: Returned data does not match polled channel\n"+hex.Dump([]byte(resp)), 1) return 0, nil, fmt.Errorf("channel missmatch") } if int(head[1]) == 1 { //sucess,message follows writeDebug(fmt.Sprintf("*** SUCCESS on channel %d: %s", ch, string(payload)), 1) if ch == NMEAChannel && s.Userconfig.GPSdAddress != "" { if s.GPSStream.GetLen() == 0 { s.GPSStream.Enqueue("$" + string(bytes.Trim(payload, "\x00"))) //need to remove the trailing NULL byte } } /* if ch == TRXControlChannel { writeDebug("TRX Control channel message: "+string(payload), 1) s.FromTRX.Enqueue(payload) } */ } if int(head[1]) == 2 { writeDebug("*** ERROR: "+string(payload), 0) } if int(head[1]) != 7 && int(head[1]) != 3 { if !s.VARAMode && ch == PactorChannel { s.Command.Response.Enqueue(fmt.Sprintf("%s\n", payload)) } writeDebug("Message from Modem: "+string(payload), 1) return 0, nil, fmt.Errorf("not a data response") } if int(head[1]) == 3 { //Link status if !s.VARAMode { s.Command.Response.Enqueue(fmt.Sprintf("%s\n", payload)) } writeDebug("*** LINK STATUS: "+string(payload), 0) // there is no way to get the remote callsign from the WA8DED data, so we have to parse the link status :( re, err := regexp.Compile(` CONNECTED to \w{2,16}`) if err != nil { writeDebug("Cannot convert connect message to callsign: "+string(payload), 1) } else { ans := strings.ReplaceAll(string(re.Find(payload)), " CONNECTED to ", "") if len(ans) > 2 { //callsign consists of 3+ characters p.remoteAddr = ans writeDebug("PACTOR connection to: "+ans, 0) if s.VARAMode { if p.state == Connecting { //outgoing connection s.Command.Response.Enqueue(fmt.Sprintf("CONNECTED %s %s BW", p.localAddr, ans)) } else { //incoming connection s.Command.Response.Enqueue(fmt.Sprintf("CONNECTED %s %s BW", ans, p.localAddr)) } } p.setState(Conntected) return 0, nil, nil } } if strings.Contains(string(payload), "DISCONNECTED") { writeDebug("PACTOR DISCONNECTED", 0) p.setState(Ready) if s.VARAMode { s.Command.Response.Enqueue("DISCONNECTED") } // } //p.channelState.f = 0 return 0, nil, nil } return 0, nil, fmt.Errorf("link data") } if length != pl { writeDebug("WARNING: Data length "+strconv.Itoa(pl)+" does not match stated amount "+strconv.Itoa(length)+". After "+strconv.Itoa(p.goodChunks)+" good chunks.", 1) p.goodChunks = 0 if pl < length { // TODO: search for propper go function for i := 0; i < (length - pl); i++ { payload = append(payload, 0x00) } } else { payload = payload[:length] } } else { p.goodChunks += 1 writeDebug("Good chunk", 2) } return length, payload, nil } // Write and read response from pactor modem // // Can be used in both, normal and hostmode. If used in hostmode, provide // channel (>=0). If used in normal mode, set channel to -1, isCommand is // ignored. func (p *Modem) writeAndGetResponse(msg string, ch int, isCommand bool, chunkSize int) (int, string, error) { writeDebug("wagr: Channel: "+strconv.Itoa(ch)+"; isCommand: "+strconv.FormatBool(isCommand)+"\n"+hex.Dump([]byte(msg)), 3) var err error var n int var str string if ch < 0 { err = p.write(msg + "\r") if err != nil { return 0, "", err } time.Sleep(500 * time.Millisecond) i := 0 var tmp []byte for { n, tmp, err = p.read(chunkSize) str = string(tmp) if err == nil { break } i += 1 if i > 9 { writeDebug("No successful read after 10 times!", 1) return 0, "", err } } writeDebug(fmt.Sprintf("response: %s\n%s", str, hex.Dump(tmp)), 2) return n, str, err } else { err = p.writeChannel(msg, ch, isCommand) if err != nil { return 0, "", err } if msg == "JHOST0" { //looks like the SCS modems do not answer to JHOST0, although the standard defines it return 0, "", nil } time.Sleep(50 * time.Millisecond) writeDebug("Decode WA8DED", 4) var buf []byte valid := false for valid == false { if bytes.Compare(buf, []byte{170, 170, 170, 85}) == 0 { // check for re-request #170#170#170#85 // packet was re-requested!! writeDebug("Re-Request magic received", 3) buf = []byte{} //delete the re-request packet err = p.writeChannel(msg, ch, isCommand) // write command again if err != nil { return 0, "", err } } br, b, err := p.read(-1) if err != nil { writeDebug("ERROR at _read: "+error.Error(err), 1) } writeDebug("Len: "+strconv.Itoa(len(buf))+" State: "+hex.Dump(buf)+"\n", 4) if br > 0 { //we got some data buf = append(buf, b...) if len(buf) > 5 { // otherwise it's no enh. WA8DED: 2 magic bytes, 2 header bytes, 2 CRC bytes //unstuff (from 3rd byte on) t := bytes.Replace(buf[2:], []byte{170, 0}, []byte{170}, -1) buf = append([]byte{0xaa, 0xaa}, t...) if checkcrc(buf) { n = len(t) - 2 str = string(t[:n]) valid = true } else { writeDebug("(still) Invalid checksum", 4) } } } } } p.packetcounter = !(p.packetcounter) writeDebug("wagr: returning \n"+hex.Dump([]byte(str)), 3) return n, str, err } // Flush waits for the last frames to be transmitted. // // Will throw error if remaining frames could not bet sent within 120s func (p *Modem) Flush() (err error) { writeDebug("Flush called", 2) if err = p.waitTransmissionFinish(120 * time.Second); err != nil { writeDebug(err.Error(), 2) } return } // TxBufferLen returns the number of bytes in the out sendbuf queue. func (p *Modem) TxBufferLen() int { writeDebug("TxBufferLen called ("+strconv.Itoa(s.Data.Data.GetLen())+" bytes remaining in sendbuf)", 2) return s.Data.Data.GetLen() + (p.getNumFramesNotTransmitted() * MaxSendData) } func (p *Modem) Busy() bool { return p.chanbusy } // *** Helper functions for the CRC hostmode // Although these functions are small, I prefer to keep their functionality // separate. They follow the steps in the SCS documentation /* // unstuff: helper function: "unstuff" a string func unstuff(s string) string { //Expect: the string contains "aa aa" at the beginning, that should NOT be //stuffed n, _ := hex.DecodeString(s[4:]) n = bytes.Replace(n, []byte{170, 0}, []byte{170}, -1) var r []byte r = append([]byte{0xaa, 0xaa}, n...) re := hex.EncodeToString(r) return re } */ /* // stuff: helper function: "stuff" a string: replaces every #170 with #170#0 func stuff(s string) string { //Expect: the string contains "aa aa" at the beginning, that should NOT be //stuffed n, err := hex.DecodeString(s[4:]) if err != nil { writeDebug("ERROR in Stuff: "+err.Error()+"\n"+hex.Dump([]byte(s)), 1) } n = bytes.Replace(n, []byte{170}, []byte{170, 0}, -1) var r []byte r = append([]byte{0xaa, 0xaa}, n...) re := hex.EncodeToString(r) return re } */ /* // checksum: helper function: calculates the CCITT-CRC16 checksum func checksum(s string) uint16 { tochecksum, _ := hex.DecodeString(s[4:]) chksum := bits.ReverseBytes16(crc16.ChecksumCCITT(tochecksum)) return chksum } */ // checkcrc: helper fuction: check the checksum by comparing func checkcrc(s []byte) bool { tochecksum := s[2 : len(s)-2] chksum := bits.ReverseBytes16(crc16.ChecksumCCITT(tochecksum)) pksum := s[len(s)-2:] return binary.BigEndian.Uint16(pksum) == chksum } /* // docrc: super helper fuction: convert an ordinary WA8DED message into a CRC-Hostmode message func docrc(msg string) string { // step 1: add a #170170 msg = fmt.Sprintf("%02x%02x%s", 170, 170, msg) // step 2: calculate and add the checksum chksum := checksum(msg) msg = fmt.Sprintf("%s%04x", msg, chksum) // step 3: add "stuff" bytes msg = stuff(msg) return msg } */ // writeChannel: Write channel to serial connection (NOT thread safe)- If used, make sure to mutex! func (p *Modem) writeChannel(msg string, ch int, isCommand bool) error { if err := p.checkSerialDevice(); err != nil { writeDebug(err.Error(), 1) return err } var d byte switch { case !p.packetcounter && !isCommand: d = byte(0x00) case !p.packetcounter && isCommand: d = byte(0x01) case p.packetcounter && !isCommand: d = byte(0x80) case p.packetcounter && isCommand: d = byte(0x81) } o := []byte{170, 170, byte(ch), d, byte(len(msg) - 1)} o = append(o, []byte(msg)...) cksum := bits.ReverseBytes16(crc16.ChecksumCCITT(o[2:])) cksumb := make([]byte, 2) binary.BigEndian.PutUint16(cksumb, cksum) o = append(o, cksumb...) tostuff := o[2:] tostuff = bytes.Replace(tostuff, []byte{170}, []byte{170, 0}, -1) o = append([]byte{170, 170}, tostuff...) /* msg = fmt.Sprintf("%02x%02x%s", 170, 170, msg) // step 2: calculate and add the checksum chksum := checksum(msg) msg = fmt.Sprintf("%s%04x", msg, chksum) // step 3: add "stuff" bytes msg = stuff(msg) */ /*c := fmt.Sprintf("%02x", ch) l := fmt.Sprintf("%02x", (len(msg) - 1)) s := hex.EncodeToString([]byte(msg)) // add crc hostmode addons bs, _ := hex.DecodeString(docrc(fmt.Sprintf("%s%s%s%s", c, d, l, s)))*/ writeDebug("write channel: \n"+hex.Dump(o), 2) if err := p.write(string(o)); err != nil { writeDebug(err.Error(), 2) return err } writeDebug("Done writing channel", 2) /*if !isCommand { p.cmdBuf <- "%Q" }*/ return nil } // read: Read readsize devices from serial connection (thread safe). If readsize==-1 try to check how many data is there func (p *Modem) read(readsize int) (int, []byte, error) { var chunkSize int if readsize == -1 { t, err := p.device.ReadyToRead() if err != nil { chunkSize = math.MaxInt writeDebug("ERROR in ReadyToRead: "+err.Error(), 3) } else { chunkSize = int(t) writeDebug(fmt.Sprintf("chunksize: %d", chunkSize), 3) } } else { chunkSize = readsize } buf := make([]byte, chunkSize) if strings.HasPrefix(p.devicePath, "tcp://") { err := p.tcpdevice.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) if err != nil { writeDebug(err.Error(), 0) } n, err := p.tcpdevice.Read(buf) if err != nil { writeDebug("Error received during read: "+err.Error(), 1) return 0, []byte{}, err } return n, buf[0:n], nil } else { if err := p.checkSerialDevice(); err != nil { writeDebug(err.Error(), 1) return 0, []byte{}, err } p.mux.device.Lock() defer p.mux.device.Unlock() err := p.device.SetReadTimeout(100) // 100 ms if err != nil { writeDebug(err.Error(), 0) } n, err := p.device.Read(buf) if err != nil { writeDebug("Error received during read: "+err.Error(), 1) return 0, []byte{}, err } return n, buf[0:n], nil } } // Write to serial connection (thread safe) // // No other read/write operation allowed during this time func (p *Modem) write(cmd string) error { if strings.HasPrefix(p.devicePath, "tcp://") { //TCP connection out := cmd + "\r" sent, err := p.tcpdevice.Write([]byte(out)) if err == nil { return err } else { writeDebug(err.Error(), 2) out = out[sent:] return nil } } else { //Serial connection if err := p.checkSerialDevice(); err != nil { writeDebug(err.Error(), 1) return err } writeDebug("write: \n"+hex.Dump([]byte(cmd)), 3) p.mux.device.Lock() defer p.mux.device.Unlock() out := cmd + "\r" for { // check if the serial line is clear-to-send status, err := cts(p.device) if err != nil { writeDebug("GetModemStatusBits failed. cmd: "+cmd+" Error: "+err.Error(), 1) return err } if status { for { sent, err := p.device.Write([]byte(out)) if err == nil { break } else { writeDebug(err.Error(), 2) out = out[sent:] } } break } } return nil } }