VModem is a Go library that implements a virtual Hayes-compatible modem simulator over TCP/IP networks. It provides complete AT command processing, state management, and network connectivity for legacy systems that need to communicate over modern networks.
- Complete Hayes AT Command Set: Supports standard AT commands including dial, answer, hangup, configuration, and S-registers
- State Machine Management: Robust modem state transitions (Detached, Idle, Dialing, Connected, Ringing, etc.)
- TCP/IP Transport: Routes modem calls over modern TCP/IP networks
- Virtual TTY Support: Creates pseudo-terminals for legacy application compatibility
- Metrics and Monitoring: Built-in statistics tracking with optional HTTP endpoint
- Concurrent Operations: Thread-safe design with proper goroutine management
- Configurable Behavior: Extensive configuration options for timeouts, guards, and responses
- Extensible Hooks: Custom command processing and line hooks for specialized behavior
go get github.com/jaracil/vmodempackage main
import (
"log"
"github.com/jaracil/vmodem"
)
func main() {
// Create a mock TTY for this example
tty := &MockTTY{}
config := &vmodem.ModemConfig{
Id: "my-modem",
TTY: tty,
ConnectStr: "CONNECT 9600",
RingMax: 10,
}
modem, err := vmodem.NewModem(config)
if err != nil {
log.Fatal(err)
}
defer modem.CloseSync()
// Process AT commands
result := modem.ProcessAtCommandSync("ATE1")
log.Printf("Command result: %v", result)
}- Modem Engine: Central state machine handling AT commands and modem behavior
- TTY Interface: Virtual terminal integration for legacy compatibility
- Network Layer: TCP/IP transport for modern connectivity
- Command Parser: Full AT command syntax support with chaining and validation
- Metrics System: Runtime statistics and performance monitoring
The modem implements a strict state machine with the following states:
Detached → Idle → Dialing → Connected ⇄ ConnectedCmd
↓ ↓
Ringing → Connected
↓
Closed (terminal state)
- Detached: No TTY client is connected (initial state)
- Idle: TTY client connected, ready to accept commands
- Dialing: Outgoing call in progress
- Connected: Active data connection (online mode)
- ConnectedCmd: Active connection in command mode (after escape sequence)
- Ringing: Incoming call being signaled
- Closed: Modem permanently closed (terminal state)
- Basic Commands:
E(echo),V(verbose),Q(quiet),H(hangup) - Connection:
D(dial),A(answer),O(online) - Configuration:
Sregisters,&F(factory reset),Z(reset) - Advanced: Command chaining,
A/(repeat last command)
type ModemConfig struct {
Id string // Modem identifier
TTY io.ReadWriteCloser // TTY interface
OutgoingCall OutgoingCallType // Dial-out handler
CommandHook CommandHookType // Custom AT command hook
LineHook LineHookType // Complete command line hook
StatusTransition StatusTransitionType // State change notifications
ConnectStr string // Connect response string
RingMax int // Maximum rings before timeout
AnswerChar string // Answer character to send/expect
GuardTime int // Escape sequence guard time
DisablePreGuard bool // Disable pre-guard time
DisablePostGuard bool // Disable post-guard time
}Customize modem behavior with hook functions:
// Custom AT command processing (individual commands)
func commandHook(m *vmodem.Modem, cmdChar string, cmdNum string,
cmdAssign bool, cmdQuery bool, cmdAssignVal string) vmodem.RetCode {
if cmdChar == "I" && cmdNum == "0" {
m.TtyWriteStr("VModem v1.0")
return vmodem.RetCodeOk
}
return vmodem.RetCodeSkip // Let default processing handle it
}
// Complete command line processing (before parsing)
func lineHook(m *vmodem.Modem, line string) vmodem.RetCode {
if strings.HasPrefix(line, "CUSTOM") {
// Handle custom command
m.TtyWriteStr("Custom command executed")
return vmodem.RetCodeOk
}
return vmodem.RetCodeSkip // Let default parsing handle it
}
// Outgoing call handler
func outgoingCall(m *vmodem.Modem, number string) (io.ReadWriteCloser, error) {
return net.Dial("tcp", translateNumber(number))
}See cmd/vmodem for a complete reference implementation that demonstrates:
- Virtual TTY creation and management
- TCP server for incoming connections
- Phone number translation patterns
- Command-line configuration
- Metrics HTTP endpoint
- Serial port integration
- Production-ready deployment
The reference implementation serves as both a working virtual modem server and a comprehensive example of how to use this library.
The library includes comprehensive unit tests covering:
- Pure function testing
- State machine transitions
- AT command processing (both direct and TTY flow)
- Concurrent operations
- Error handling
Run tests with:
go test ./...The library provides detailed runtime metrics:
type Metrics struct {
Status ModemStatus // Current modem state
TtyTxBytes int // Bytes transmitted to TTY
TtyRxBytes int // Bytes received from TTY
ConnTxBytes int // Bytes transmitted to network
ConnRxBytes int // Bytes received from network
NumConns int // Total connections
NumInConns int // Incoming connections
NumOutConns int // Outgoing connections
LastTtyTxTime time.Time // Last TTY transmission
LastTtyRxTime time.Time // Last TTY reception
LastAtCmdTime time.Time // Last AT command
LastConnTime time.Time // Last connection time
}The reference implementation (cmd/vmodem) provides an optional HTTP metrics endpoint that exposes modem statistics in JSON format, including:
- Current modem status (Detached, Idle, Connected, etc.)
- Byte counters for TTY and network traffic
- Connection counts (total, incoming, outgoing)
- Timestamps of last activities
Access metrics via HTTP GET request to the configured metrics address (e.g., http://localhost:8080/metrics/modem-id).
The library defines specific error types:
ErrConfigRequired: Invalid or missing configurationErrModemBusy: Modem unavailable for new operationsErrInvalidStateTransition: Illegal state change attemptedErrNoCarrier: Connection failed or lost
All public methods are thread-safe and provide both synchronous and asynchronous variants:
Status()/StatusSync(): Get modem stateSetStatus()/SetStatusSync(): Change modem stateProcessAtCommand()/ProcessAtCommandSync(): Execute AT commandsIncomingCall()/IncomingCallSync(): Handle incoming connections
Complete API documentation is available at pkg.go.dev.
The core vmodem library has minimal dependencies and can be used standalone.
The complete modem server implementation uses the following dependencies:
github.com/creack/pty- PTY (pseudo-terminal) creation and managementgithub.com/jaracil/nagle- Nagle buffering for network optimizationgithub.com/jessevdk/go-flags- Command-line argument parsinggithub.com/nayarsystems/iotrace- I/O tracing for debugginggo.bug.st/serial- Serial port communication
This project is licensed under the MIT License.
Contributions are welcome! Please ensure all tests pass and follow the existing code style.
For issues and questions, please use the GitHub issue tracker.