package main import ( "bufio" "context" "fmt" "io" "regexp" "strconv" "strings" "time" "github.com/gordonklaus/portaudio" "github.com/naleek/gortty" ) type Modem struct { ttyReader io.Reader ttyWriter io.Writer modulator *gortty.Modulator demodulator *gortty.Demodulator encoder *gortty.Encoder decoder *gortty.Decoder stream *portaudio.Stream registers map[int]configRegister autoAnswer bool escapeRune rune modulatorOn bool turnaroundDelay int usos bool localEcho bool } func (m *Modem) Close() { m.stream.Close() portaudio.Terminate() } func (m *Modem) SetLocalEcho(e bool) { m.localEcho = e } func (m *Modem) GetRegister(index int) (string, bool) { if r, ok := m.registers[index]; ok { return r.f(registerGet, ""), true } return "", false } func (m *Modem) SetRegister(index int, value string) bool { if r, ok := m.registers[index]; ok { r.f(registerSet, value) return true } else { return false } } func NewModem(ctx context.Context, rttySettings *gortty.ModemSettings, charset *gortty.CodeSet) (*Modem, error) { m := &Modem{} m.registers = map[int]configRegister{ 0: { help: "auto-answer enable", f: func(action int, value string) string { switch action { case registerGet: if m.autoAnswer { return "1" } else { return "0" } case registerSet: m.autoAnswer = (value == "1") return "OK" } return "" }, }, 2: { help: "escape character", f: func(action int, value string) string { switch action { case registerGet: return string(m.escapeRune) case registerSet: runes := []rune(value) m.escapeRune = runes[0] return "OK" } return "" }, }, 101: { help: "modulator enable", f: func(action int, value string) string { switch action { case registerGet: if m.modulatorOn { return "1" } else { return "0" } case registerSet: m.modulatorOn = (value == "1") if m.modulator != nil { if m.modulatorOn { m.modulator.SetAmplitude(16384) } else { m.modulator.SetAmplitude(0) } } return "OK" } return "" }, }, 102: { help: "turnaround delay", f: func(action int, value string) string { switch action { case registerGet: return fmt.Sprintln(m.turnaroundDelay) case registerSet: if d, err := strconv.Atoi(value); err == nil { m.turnaroundDelay = d return "OK" } else { return "ERROR" } } return "" }, }, 103: { help: "unshift on space", f: func(action int, value string) string { switch action { case registerGet: if m.usos { return "1" } else { return "0" } case registerSet: m.usos = (value == "1") m.decoder.UnshiftOnSpace(m.usos) return "OK" } return "" }, }, } m.autoAnswer = false m.escapeRune = '?' m.modulatorOn = true m.turnaroundDelay = 0 m.usos = false in := make([]int16, 64) out := make([]int16, 64) // Set up audio with system default input and output portaudio.Initialize() var err error m.stream, err = portaudio.OpenDefaultStream(1, 1, float64(rttySettings.SampleRate), len(in), in, out) if err != nil { panic(err) } modulatorToAudio := make(chan int16) audioToDemodulator := make(chan int16) // Set up modulator pipeline encoderToModulator := make(chan byte) m.encoder = gortty.NewEncoder(ctx, nil, encoderToModulator, charset) m.modulator = gortty.NewModulator(ctx, rttySettings, encoderToModulator, modulatorToAudio) m.modulator.SetIdleDelay(1000) // Set up demodulator pipeline demodulatorToDecoder := make(chan byte) m.demodulator = gortty.NewDemodulator(ctx, rttySettings, audioToDemodulator, demodulatorToDecoder) m.decoder = gortty.NewDecoder(ctx, demodulatorToDecoder, nil, charset) // start audio m.stream.Start() // Audio output go func() { l := 0 for v := range modulatorToAudio { out[l] = v l++ if l == 64 { l = 0 m.stream.Write() } } }() // Audio input go func() { for { m.stream.Read() for i := range in { audioToDemodulator <- in[i] } } }() return m, nil } func (m *Modem) modulatorCallback(event int, data interface{}) { switch event { case gortty.EventActive: fmt.Println("EventActive") m.demodulator.Inhibit(true) case gortty.EventIdle: fmt.Println("EventIdle") m.demodulator.Inhibit(false) } } type configFunction func(action int, value string) string type configRegister struct { help string f configFunction } const ( registerGet = iota registerSet ) func (m *Modem) run() { ttyBufferedReader := bufio.NewReader(m.decoder) var netConn *NetConn if m.modulator != nil { m.modulator.SetCallback(m.modulatorCallback) } for { s, _ := ttyBufferedReader.ReadString('\n') //fmt.Println(s) s = strings.ToUpper(s) regex, _ := regexp.Compile(`^AT([SHOED?])([0-9])?(\?)?(?:[=|\:]([\S]*))?(?: (.*))?`) params := regex.FindStringSubmatch(s) if params == nil { continue } command := params[1] var register string if len(params) > 2 { register = params[2] } var registerQuery string if len(params) > 3 { registerQuery = params[3] } var value string if len(params) > 4 { value = params[4] } var parameter string if len(params) > 5 { parameter = params[5] } //fmt.Fprintf(m.encoder, "command: '%s' register: '%s' query: '%s' value: '%s' parameter: '%s'\n", command, register, registerQuery, value, parameter) switch command { case "?": fmt.Fprintf(m.encoder, "Setting Registers\n") for s, conf := range m.registers { fmt.Fprintf(m.encoder, "%s: %s (current value: %s)\n", s, conf.help, conf.f(registerGet, "")) } case "O": // online if !netConn.Connected() { fmt.Fprintln(m.encoder, "ERROR No active session") break } fmt.Fprintln(m.encoder, "OK") dataMode(netConn, ttyBufferedReader, m.encoder, m.escapeRune) case "D": // dial if netConn.Connected() { fmt.Fprintln(m.encoder, "ERROR Session in progress") break } addr := strings.TrimSpace(parameter) var err error netConn, err = NewNetConn(addr) if err != nil { fmt.Fprintln(m.encoder, "ERROR", err) break } else { fmt.Fprintln(m.encoder, "CONNECTED") dataMode(netConn, ttyBufferedReader, m.encoder, m.escapeRune) } case "H": // hangup if !netConn.Connected() { fmt.Fprintln(m.encoder, "ERROR No active session") break } netConn.Hangup() fmt.Fprintln(m.encoder, "OK") case "E": // local echo m.localEcho = (register == "1") fmt.Fprintln(m.encoder, "local echo", m.localEcho) case "S": // register r, _ := strconv.Atoi(register) if registerQuery == "?" { // get if v, ok := m.GetRegister(r); ok { fmt.Fprintln(m.encoder, v) } else { fmt.Fprintln(m.encoder, "ERROR") } } else if value != "" { //set if m.SetRegister(r, value) { fmt.Fprintln(m.encoder, "OK") } else { fmt.Fprintln(m.encoder, "ERROR") } } else { fmt.Fprintln(m.encoder, "ERROR") } } } } func dataMode(netConn *NetConn, ttyBufferedReader *bufio.Reader, ttyWriter io.Writer, escapeRune rune) { ttyCancelReader := make(chan struct{}) ttyReadChan := make(chan rune) ttyReadPending := make(chan struct{}) var t *time.Timer escapeRunes := 0 go func() { for { r, l, err := ttyBufferedReader.ReadRune() if err != nil && err != io.EOF { return } if l > 0 { select { case <-ttyCancelReader: ttyBufferedReader.UnreadRune() // oops this wasn't ours. let's put it back. close(ttyReadPending) return default: ttyReadChan <- r } } } }() netReadChan := netConn.OutputChannel() netConn.Resume() pipeLoop: for { var r rune if t == nil { select { case r = <-ttyReadChan: case q := <-netReadChan: ttyWriter.Write([]byte(string(q))) continue pipeLoop } } else { select { case r = <-ttyReadChan: case q := <-netReadChan: ttyWriter.Write([]byte(string(q))) continue pipeLoop case <-t.C: fmt.Fprintln(ttyWriter, "\nOK") close(ttyCancelReader) netConn.Pause() break pipeLoop } } if r == escapeRune && escapeRunes < 3 { escapeRunes++ } else { if t != nil { t.Stop() } escapeRunes = 0 } if escapeRunes == 3 { t = time.NewTimer(1 * time.Second) } err := netConn.Write(r) if err != nil { fmt.Fprintln(ttyWriter, "NO CARRIER") close(ttyCancelReader) break pipeLoop } } <-ttyReadPending }