├── .travis.yml ├── README.md ├── examples ├── receive │ └── receive.go └── stored │ └── stored.go ├── gsm.go ├── gsm_test.go ├── mock.go ├── packets.go ├── util.go └── util_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.8 4 | 5 | script: 6 | - go test -v ./... 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## gogsmmodem: Go library for GSM modems 2 | 3 | [![Build Status](https://travis-ci.org/barnybug/gogsmmodem.svg?branch=master)](https://travis-ci.org/barnybug/gogsmmodem) 4 | 5 | Go library for the sending and receiving SMS messages through a GSM modem. 6 | 7 | ### Tested devices 8 | - ZTE MF110/MF627/MF636 9 | 10 | ### Installation 11 | Run: 12 | 13 | go get github.com/barnybug/gogsmmodem 14 | 15 | ### Usage 16 | Example: 17 | 18 | ```go 19 | 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/barnybug/gogsmmodem" 26 | "github.com/tarm/serial" 27 | ) 28 | 29 | func main() { 30 | conf := serial.Config{Name: "/dev/ttyUSB1", Baud: 115200} 31 | modem, err := gogsmmodem.Open(&conf, true) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | for packet := range modem.OOB { 37 | fmt.Printf("%#v\n", packet) 38 | switch p := packet.(type) { 39 | case gogsmmodem.MessageNotification: 40 | fmt.Println("Message notification:", p) 41 | msg, err := modem.GetMessage(p.Index) 42 | if err == nil { 43 | fmt.Printf("Message from %s: %s\n", msg.Telephone, msg.Body) 44 | modem.DeleteMessage(p.Index) 45 | } 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ### Changelog 52 | 0.1.0 53 | 54 | - First release 55 | -------------------------------------------------------------------------------- /examples/receive/receive.go: -------------------------------------------------------------------------------- 1 | // Example of receiving messages. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "log" 7 | 8 | "github.com/barnybug/gogsmmodem" 9 | "github.com/tarm/serial" 10 | ) 11 | 12 | func main() { 13 | conf := serial.Config{Name: "/dev/ttyUSB1", Baud: 115200} 14 | modem, err := gogsmmodem.Open(&conf, true) 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | for packet := range modem.OOB { 20 | log.Printf("Received: %#v\n", packet) 21 | switch p := packet.(type) { 22 | case gogsmmodem.MessageNotification: 23 | log.Println("Message notification:", p) 24 | msg, err := modem.GetMessage(p.Index) 25 | if err == nil { 26 | fmt.Printf("Message from %s: %s\n", msg.Telephone, msg.Body) 27 | modem.DeleteMessage(p.Index) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/stored/stored.go: -------------------------------------------------------------------------------- 1 | // Example of retrieving stored messages. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/barnybug/gogsmmodem" 8 | "github.com/tarm/serial" 9 | ) 10 | 11 | func main() { 12 | conf := serial.Config{Name: "/dev/ttyUSB1", Baud: 115200} 13 | modem, err := gogsmmodem.Open(&conf, true) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | li, _ := modem.ListMessages("ALL") 19 | fmt.Printf("%d stored messages\n", len(*li)) 20 | for _, msg := range *li { 21 | fmt.Println(msg.Index, msg.Status, msg.Body) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gsm.go: -------------------------------------------------------------------------------- 1 | package gogsmmodem 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "log" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/tarm/serial" 12 | ) 13 | 14 | type Modem struct { 15 | OOB chan Packet 16 | Debug bool 17 | port io.ReadWriteCloser 18 | rx chan Packet 19 | tx chan string 20 | } 21 | 22 | var OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 23 | return serial.OpenPort(config) 24 | } 25 | 26 | func Open(config *serial.Config, debug bool) (*Modem, error) { 27 | port, err := OpenPort(config) 28 | if debug { 29 | port = LogReadWriteCloser{port} 30 | } 31 | if err != nil { 32 | return nil, err 33 | } 34 | oob := make(chan Packet, 16) 35 | rx := make(chan Packet) 36 | tx := make(chan string) 37 | modem := &Modem{ 38 | OOB: oob, 39 | Debug: debug, 40 | port: port, 41 | rx: rx, 42 | tx: tx, 43 | } 44 | // run send/receive goroutine 45 | go modem.listen() 46 | 47 | err = modem.init() 48 | if err != nil { 49 | return nil, err 50 | } 51 | return modem, nil 52 | } 53 | 54 | func (self *Modem) Close() error { 55 | close(self.OOB) 56 | close(self.rx) 57 | // close(self.tx) 58 | return self.port.Close() 59 | } 60 | 61 | // Commands 62 | 63 | // GetMessage by index n from memory. 64 | func (self *Modem) GetMessage(n int) (*Message, error) { 65 | packet, err := self.send("+CMGR", n) 66 | if err != nil { 67 | return nil, err 68 | } 69 | if msg, ok := packet.(Message); ok { 70 | return &msg, nil 71 | } 72 | return nil, errors.New("Message not found") 73 | } 74 | 75 | // ListMessages stored in memory. Filter should be "ALL", "REC UNREAD", "REC READ", etc. 76 | func (self *Modem) ListMessages(filter string) (*MessageList, error) { 77 | packet, err := self.send("+CMGL", filter) 78 | if err != nil { 79 | return nil, err 80 | } 81 | res := MessageList{} 82 | if _, ok := packet.(OK); ok { 83 | // empty response 84 | return &res, nil 85 | } 86 | 87 | for { 88 | if msg, ok := packet.(Message); ok { 89 | res = append(res, msg) 90 | if msg.Last { 91 | break 92 | } 93 | } else { 94 | return nil, errors.New("Unexpected error") 95 | } 96 | 97 | packet = <-self.rx 98 | } 99 | return &res, nil 100 | } 101 | 102 | func (self *Modem) SupportedStorageAreas() (*StorageAreas, error) { 103 | packet, err := self.send("+CPMS", "?") 104 | if err != nil { 105 | return nil, err 106 | } 107 | if msg, ok := packet.(StorageAreas); ok { 108 | return &msg, nil 109 | } 110 | return nil, errors.New("Unexpected response type") 111 | } 112 | 113 | func (self *Modem) DeleteMessage(n int) error { 114 | _, err := self.send("+CMGD", n) 115 | return err 116 | } 117 | 118 | func (self *Modem) SendMessage(telephone, body string) error { 119 | enc := gsmEncode(body) 120 | _, err := self.sendBody("+CMGS", enc, telephone) 121 | return err 122 | } 123 | 124 | func lineChannel(r io.Reader) chan string { 125 | ret := make(chan string) 126 | go func() { 127 | buffer := bufio.NewReader(r) 128 | for { 129 | line, _ := buffer.ReadString(10) 130 | line = strings.TrimRight(line, "\r\n") 131 | if line == "" { 132 | continue 133 | } 134 | ret <- line 135 | } 136 | }() 137 | return ret 138 | } 139 | 140 | var reQuestion = regexp.MustCompile(`AT(\+[A-Z]+)`) 141 | 142 | func isFinalStatus(status string) bool { 143 | return status == "OK" || 144 | status == "ERROR" || 145 | strings.Contains(status, "+CMS ERROR") || 146 | strings.Contains(status, "+CME ERROR") 147 | } 148 | 149 | func parsePacket(status, header, body string) Packet { 150 | if header == "" && isFinalStatus(status) { 151 | if status == "OK" { 152 | return OK{} 153 | } else { 154 | return ERROR{} 155 | } 156 | } 157 | 158 | ls := strings.SplitN(header, ":", 2) 159 | if len(ls) != 2 { 160 | return UnknownPacket{header, []interface{}{}} 161 | } 162 | uargs := strings.TrimSpace(ls[1]) 163 | args := unquotes(uargs) 164 | switch ls[0] { 165 | case "+ZUSIMR": 166 | // message storage unset nag, ignore 167 | return nil 168 | case "+ZPASR": 169 | return ServiceStatus{args[0].(string)} 170 | case "+ZDONR": 171 | return NetworkStatus{args[0].(string)} 172 | case "+CMTI": 173 | return MessageNotification{args[0].(string), args[1].(int)} 174 | case "+CSCA": 175 | return SMSCAddress{args} 176 | case "+CMGR": 177 | return Message{Status: args[0].(string), Telephone: args[1].(string), 178 | Timestamp: parseTime(args[3].(string)), Body: body} 179 | case "+CMGL": 180 | return Message{ 181 | Index: args[0].(int), 182 | Status: args[1].(string), 183 | Telephone: args[2].(string), 184 | Timestamp: parseTime(args[4].(string)), 185 | Body: body, 186 | Last: status != "", 187 | } 188 | case "+CPMS": 189 | s := uargs 190 | if strings.HasPrefix(s, "(") { 191 | // query response 192 | // ("A","B","C"),("A","B","C"),("A","B","C") 193 | s = strings.TrimPrefix(s, "(") 194 | s = strings.TrimSuffix(s, ")") 195 | areas := strings.SplitN(s, "),(", 3) 196 | return StorageAreas{ 197 | stringsUnquotes(areas[0]), 198 | stringsUnquotes(areas[1]), 199 | stringsUnquotes(areas[2]), 200 | } 201 | } else { 202 | // set response 203 | // 0,100,0,100,0,100 204 | // get ints 205 | var iargs []int 206 | for _, arg := range args { 207 | if iarg, ok := arg.(int); ok { 208 | iargs = append(iargs, iarg) 209 | } 210 | } 211 | if len(iargs) == 6 { 212 | return StorageInfo{ 213 | iargs[0], iargs[1], iargs[2], iargs[3], iargs[4], iargs[5], 214 | } 215 | } else if len(iargs) == 4 { 216 | return StorageInfo{ 217 | iargs[0], iargs[1], iargs[2], iargs[3], 0, 0, 218 | } 219 | } 220 | break 221 | 222 | } 223 | case "": 224 | if status == "OK" { 225 | return OK{} 226 | } else { 227 | return ERROR{} 228 | } 229 | } 230 | return UnknownPacket{ls[0], args} 231 | } 232 | 233 | func (self *Modem) listen() { 234 | in := lineChannel(self.port) 235 | var echo, last, header, body string 236 | for { 237 | select { 238 | case line := <-in: 239 | if line == echo { 240 | continue // ignore echo of command 241 | } else if last != "" && startsWith(line, last) { 242 | if header != "" { 243 | // first of multiple responses (eg CMGL) 244 | packet := parsePacket("", header, body) 245 | self.rx <- packet 246 | } 247 | header = line 248 | body = "" 249 | } else if isFinalStatus(line) { 250 | packet := parsePacket(line, header, body) 251 | self.rx <- packet 252 | header = "" 253 | body = "" 254 | } else if header != "" { 255 | // the body following a header 256 | body += line 257 | } else if line == "> " { 258 | // raw mode for body 259 | } else { 260 | // OOB packet 261 | p := parsePacket("OK", line, "") 262 | if p != nil { 263 | self.OOB <- p 264 | } 265 | } 266 | case line := <-self.tx: 267 | m := reQuestion.FindStringSubmatch(line) 268 | if len(m) > 0 { 269 | last = m[1] 270 | } 271 | echo = strings.TrimRight(line, "\r\n") 272 | self.port.Write([]byte(line)) 273 | } 274 | } 275 | } 276 | 277 | func formatCommand(cmd string, args ...interface{}) string { 278 | line := "AT" + cmd 279 | if len(args) > 0 { 280 | line += "=" + quotes(args) 281 | } 282 | line += "\r\n" 283 | return line 284 | } 285 | 286 | func (self *Modem) sendBody(cmd string, body string, args ...interface{}) (Packet, error) { 287 | self.tx <- formatCommand(cmd, args...) 288 | self.tx <- body + "\x1a" 289 | response := <-self.rx 290 | if _, e := response.(ERROR); e { 291 | return response, errors.New("Response was ERROR") 292 | } 293 | return response, nil 294 | } 295 | 296 | func (self *Modem) send(cmd string, args ...interface{}) (Packet, error) { 297 | self.tx <- formatCommand(cmd, args...) 298 | response := <-self.rx 299 | if _, e := response.(ERROR); e { 300 | return response, errors.New("Response was ERROR") 301 | } 302 | return response, nil 303 | } 304 | 305 | func (self *Modem) init() error { 306 | // clear settings 307 | if _, err := self.send("Z"); err != nil { 308 | return err 309 | } 310 | log.Println("Reset") 311 | // turn off echo 312 | if _, err := self.send("E0"); err != nil { 313 | return err 314 | } 315 | log.Println("Echo off") 316 | // use combined storage (MT) 317 | msg, err := self.send("+CPMS", "SM", "SM", "SM") 318 | if err != nil { 319 | msg, err = self.send("+CPMS", "SM", "SM") 320 | if err != nil { 321 | return err 322 | } 323 | } 324 | sinfo := msg.(StorageInfo) 325 | log.Printf("Set SMS Storage: %d/%d used\n", sinfo.UsedSpace1, sinfo.MaxSpace1) 326 | // set SMS text mode - easiest to implement. Ignore response which is 327 | // often a benign error. 328 | self.send("+CMGF", 1) 329 | 330 | log.Println("Set SMS text mode") 331 | // get SMSC 332 | // the modem complains if SMSC hasn't been set, but stores it correctly, so 333 | // query for stored value, then send a set from the query response. 334 | r, err := self.send("+CSCA?") 335 | if err != nil { 336 | return err 337 | } 338 | smsc := r.(SMSCAddress) 339 | log.Println("Got SMSC:", smsc.Args) 340 | r, err = self.send("+CSCA", smsc.Args...) 341 | if err != nil { 342 | return err 343 | } 344 | log.Println("Set SMSC to:", smsc.Args) 345 | return nil 346 | } 347 | -------------------------------------------------------------------------------- /gsm_test.go: -------------------------------------------------------------------------------- 1 | package gogsmmodem 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "github.com/tarm/serial" 12 | ) 13 | 14 | var initReplay = []string{ 15 | "->ATZ\r\n", 16 | "<-\r\nOK\r\n", 17 | "->ATE0\r\n", 18 | "<-ATE0\n", 19 | "<-\r\nOK\r\n", 20 | "->AT+CPMS=\"SM\",\"SM\",\"SM\"\r\n", 21 | "<-\r\n+CPMS: 50,50,50,50,50,50\r\nOK\n\n", 22 | "->AT+CMGF=1\r\n", 23 | "<-\r\nOK\r\n", 24 | "->AT+CSCA?\r\n", 25 | "<-\r\n+CSCA: \"+447802092035\",145\r\nOK\r\n", 26 | "->AT+CSCA=\"+447802092035\",145\r\n", 27 | "<-\r\nOK\r\n", 28 | } 29 | 30 | func appendLists(ls ...[]string) []string { 31 | size := 0 32 | for _, l := range ls { 33 | size += len(l) 34 | } 35 | ret := make([]string, size) 36 | off := ret 37 | for _, l := range ls { 38 | copy(off, l) 39 | off = off[len(l):] 40 | } 41 | return ret 42 | } 43 | 44 | func TestInit(t *testing.T) { 45 | OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 46 | return NewMockSerialPort(appendLists(initReplay)), nil 47 | } 48 | modem, err := Open(&serial.Config{}, true) 49 | if err != nil { 50 | t.Error("Expected: no error, got:", err) 51 | } 52 | modem.Close() 53 | } 54 | 55 | func assertOOBCommands(t *testing.T, modem *Modem, commands []Packet) { 56 | for i := range modem.OOB { 57 | if len(commands) == 0 { 58 | t.Errorf("Unexpected extra command: %#v", i) 59 | break 60 | } 61 | head := commands[0] 62 | if !reflect.DeepEqual(i, head) { 63 | t.Errorf("Expected: %#v, got: %#v", head, i) 64 | } 65 | commands = commands[1:] 66 | } 67 | if len(commands) > 0 { 68 | t.Errorf("Expected: %d more commands", len(commands)) 69 | } 70 | } 71 | 72 | var oobReplay = []string{ 73 | "<-\r\n+ZUSIMR:2\r\n", 74 | "<-\r\n+ZPASR: \"No Service\"\r\n", 75 | "<-\r\n+ZDONR: \"O2-UK\",234,10,\"CS_PS\",\"ROAM_OFF\"\r\n", 76 | "<-\r\n+ZPASR: \"EDGE\"\r\n", 77 | "<-\r\n+ZPASR: \"UMTS\"\r\n", 78 | "<-\r\nDODGY\r\n", 79 | "<-\r\n+ZZZ: \"A\"\r\n", 80 | } 81 | 82 | var oobCommands = []Packet{ 83 | ServiceStatus{"No Service"}, 84 | NetworkStatus{"O2-UK"}, 85 | ServiceStatus{"EDGE"}, 86 | ServiceStatus{"UMTS"}, 87 | UnknownPacket{"DODGY", []interface{}{}}, 88 | UnknownPacket{"+ZZZ", []interface{}{"A"}}, 89 | } 90 | 91 | func TestOOB(t *testing.T) { 92 | OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 93 | replay := appendLists(oobReplay, initReplay) 94 | return NewMockSerialPort(replay), nil 95 | } 96 | modem, err := Open(&serial.Config{}, true) 97 | if err != nil { 98 | t.Error("Expected: no error, got:", err) 99 | } 100 | modem.Close() 101 | assertOOBCommands(t, modem, oobCommands) 102 | } 103 | 104 | var receivedReplay = []string{ 105 | "<-\r\n+CMTI: \"SM\",5\r\n", 106 | } 107 | 108 | var receivedCommands = []Packet{ 109 | MessageNotification{"SM", 5}, 110 | } 111 | 112 | func TestIncoming(t *testing.T) { 113 | OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 114 | replay := appendLists(initReplay, receivedReplay) 115 | return NewMockSerialPort(replay), nil 116 | } 117 | modem, err := Open(&serial.Config{}, true) 118 | if err != nil { 119 | t.Error("Expected: no error, got:", err) 120 | } 121 | modem.Close() 122 | assertOOBCommands(t, modem, receivedCommands) 123 | } 124 | 125 | var messageReplay = []string{ 126 | "->AT+CMGR=1\r\n", 127 | "<-\r\n+CMGR: \"REC UNREAD\",\"+441234567890\",,\"14/02/01,15:07:43+00\"\r\nHi\r\n\r\nOK\r\n", 128 | } 129 | 130 | func TestGetMessage(t *testing.T) { 131 | OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 132 | replay := appendLists(initReplay, messageReplay) 133 | return NewMockSerialPort(replay), nil 134 | } 135 | modem, err := Open(&serial.Config{}, true) 136 | if err != nil { 137 | t.Error("Expected: no error, got:", err) 138 | } 139 | 140 | msg, _ := modem.GetMessage(1) 141 | expected := Message{0, "REC UNREAD", "+441234567890", time.Date(2014, 2, 1, 15, 7, 43, 0, time.UTC), "Hi", false} 142 | if *msg != expected { 143 | t.Errorf("Expected: %#v, got %#v", expected, msg) 144 | } 145 | modem.Close() 146 | } 147 | 148 | var missingMessageReplay = []string{ 149 | "->AT+CMGR=1\r\n", 150 | "<-\r\nOK\r\n", 151 | } 152 | 153 | func TestGetMissingMessage(t *testing.T) { 154 | OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 155 | replay := appendLists(initReplay, missingMessageReplay) 156 | return NewMockSerialPort(replay), nil 157 | } 158 | modem, err := Open(&serial.Config{}, true) 159 | if err != nil { 160 | t.Error("Expected: no error, got:", err) 161 | } 162 | 163 | _, err = modem.GetMessage(1) 164 | if fmt.Sprint(err) != "Message not found" { 165 | t.Errorf("Expected error: %#v, got %#v", err, err) 166 | } 167 | modem.Close() 168 | } 169 | 170 | var sendMessageReplay = []string{ 171 | "->AT+CMGS=\"441234567890\"\r\n", 172 | "<-> \r\n", 173 | "->Body\x00\x1a", 174 | "<-\r\nOK\r\n", 175 | } 176 | 177 | func TestSendMessage(t *testing.T) { 178 | OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 179 | replay := appendLists(initReplay, sendMessageReplay) 180 | return NewMockSerialPort(replay), nil 181 | } 182 | modem, err := Open(&serial.Config{}, true) 183 | if err != nil { 184 | t.Error("Expected: no error, got:", err) 185 | } 186 | 187 | err = modem.SendMessage("441234567890", "Body@") 188 | if err != nil { 189 | t.Error("Expected: no error, got:", err) 190 | } 191 | modem.Close() 192 | } 193 | 194 | var listMessagesReplay = []string{ 195 | "->AT+CMGL=\"ALL\"\r\n", 196 | "<-\r\n+CMGL: 0,\"REC UNREAD\",\"+441234567890\",,\"14/02/01,15:07:43+00\"\r\nHi\r\n+CMGL: 1,\"REC READ\",\"+441234567890\",,\"14/02/01,15:07:43+00\"\r\nOla\r\n+CMGL: 2,\"REC UNREAD\",\"+44123456", 197 | "<-7890\",,\"14/02/01,15:07:43+00\"\r\nJa\r\n\r\nOK\r\n", 198 | } 199 | 200 | func TestListMessages(t *testing.T) { 201 | OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 202 | replay := appendLists(initReplay, listMessagesReplay) 203 | return NewMockSerialPort(replay), nil 204 | } 205 | modem, err := Open(&serial.Config{}, true) 206 | if err != nil { 207 | t.Error("Expected: no error, got:", err) 208 | } 209 | 210 | msg, _ := modem.ListMessages("ALL") 211 | expected := MessageList{ 212 | Message{0, "REC UNREAD", "+441234567890", time.Date(2014, 2, 1, 15, 7, 43, 0, time.UTC), "Hi", false}, 213 | Message{1, "REC READ", "+441234567890", time.Date(2014, 2, 1, 15, 7, 43, 0, time.UTC), "Ola", false}, 214 | Message{2, "REC UNREAD", "+441234567890", time.Date(2014, 2, 1, 15, 7, 43, 0, time.UTC), "Ja", true}, 215 | } 216 | if len(*msg) != len(expected) { 217 | t.Errorf("Expected: %#v, got %#v", expected, msg) 218 | } 219 | for i, m := range *msg { 220 | if m != expected[i] { 221 | t.Errorf("Expected: %#v, got %#v", expected, msg) 222 | } 223 | } 224 | modem.Close() 225 | } 226 | 227 | var listMessagesEmptyReplay = []string{ 228 | "->AT+CMGL=\"ALL\"\r\n", 229 | "<-\r\nOK\r\n", 230 | } 231 | 232 | func TestListMessagesEmpty(t *testing.T) { 233 | OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 234 | replay := appendLists(initReplay, listMessagesEmptyReplay) 235 | return NewMockSerialPort(replay), nil 236 | } 237 | modem, err := Open(&serial.Config{}, true) 238 | if err != nil { 239 | t.Error("Expected: no error, got:", err) 240 | } 241 | 242 | msg, err := modem.ListMessages("ALL") 243 | log.Println("ERROR", err) 244 | expected := MessageList{} 245 | if len(*msg) != len(expected) { 246 | t.Errorf("Expected: %#v, got %#v", expected, msg) 247 | } 248 | modem.Close() 249 | } 250 | 251 | var storageAreasReplay = []string{ 252 | "->AT+CPMS=?\r\n", 253 | "<-\r\n+CPMS: (\"ME\",\"MT\",\"SM\",\"SR\"),(\"ME\",\"MT\",\"SM\",\"SR\"),(\"ME\",\"MT\",\"SM\",\"SR\")\r\n\r\nOK\r\n", 254 | } 255 | 256 | func TestSupportedStorageAreas(t *testing.T) { 257 | OpenPort = func(config *serial.Config) (io.ReadWriteCloser, error) { 258 | replay := appendLists(initReplay, storageAreasReplay) 259 | return NewMockSerialPort(replay), nil 260 | } 261 | modem, err := Open(&serial.Config{}, true) 262 | if err != nil { 263 | t.Error("Expected: no error, got:", err) 264 | } 265 | 266 | msg, _ := modem.SupportedStorageAreas() 267 | expected := StorageAreas{ 268 | []string{"ME", "MT", "SM", "SR"}, 269 | []string{"ME", "MT", "SM", "SR"}, 270 | []string{"ME", "MT", "SM", "SR"}, 271 | } 272 | if fmt.Sprint(*msg) != fmt.Sprint(expected) { 273 | t.Errorf("Expected: %#v, got %#v", expected, msg) 274 | } 275 | modem.Close() 276 | } 277 | -------------------------------------------------------------------------------- /mock.go: -------------------------------------------------------------------------------- 1 | package gogsmmodem 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type MockSerialPort struct { 9 | replay []string 10 | receive chan string 11 | } 12 | 13 | func NewMockSerialPort(replay []string) *MockSerialPort { 14 | self := &MockSerialPort{ 15 | replay: replay, 16 | receive: make(chan string, 16), 17 | } 18 | self.enqueueReads() 19 | return self 20 | } 21 | 22 | func (self *MockSerialPort) Read(b []byte) (int, error) { 23 | line := <-self.receive 24 | data := []byte(line) 25 | copy(b, data) 26 | return len(data), nil 27 | } 28 | 29 | func (self *MockSerialPort) enqueueReads() { 30 | // enqueue response(s) from replay 31 | for { 32 | if len(self.replay) == 0 || strings.Index(self.replay[0], "<-") != 0 { 33 | break 34 | } 35 | i := self.replay[0][2:] 36 | self.receive <- i 37 | self.replay = self.replay[1:] 38 | } 39 | } 40 | 41 | func (self *MockSerialPort) Write(b []byte) (int, error) { 42 | if len(self.replay) == 0 { 43 | fmt.Printf("Expected: no more interactions, got: %#v", string(b)) 44 | panic("fail") 45 | } 46 | i := self.replay[0] 47 | if strings.Index(i, "->") != 0 { 48 | fmt.Println("Replay isn't data to send:", i) 49 | panic("fail") 50 | } 51 | expected := i[2:] 52 | if expected != string(b) { 53 | fmt.Printf("Expected: %#v got: %#v", expected, string(b)) 54 | panic("fail") 55 | } 56 | self.replay = self.replay[1:] 57 | self.enqueueReads() 58 | return len(b), nil 59 | } 60 | 61 | func (self *MockSerialPort) Close() error { 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /packets.go: -------------------------------------------------------------------------------- 1 | package gogsmmodem 2 | 3 | import "time" 4 | 5 | type Packet interface{} 6 | 7 | // +ZPASR 8 | type ServiceStatus struct { 9 | Status string 10 | } 11 | 12 | // +ZDONR 13 | type NetworkStatus struct { 14 | Network string 15 | } 16 | 17 | // +CMTI 18 | type MessageNotification struct { 19 | Storage string 20 | Index int 21 | } 22 | 23 | // +CSCA 24 | type SMSCAddress struct { 25 | Args []interface{} 26 | } 27 | 28 | // +CMGR 29 | type Message struct { 30 | Index int 31 | Status string 32 | Telephone string 33 | Timestamp time.Time 34 | Body string 35 | Last bool 36 | } 37 | 38 | // +CPMS=? 39 | type StorageAreas struct { 40 | Received []string 41 | Sent []string 42 | New []string 43 | } 44 | 45 | // +CPMS=... 46 | type StorageInfo struct { 47 | UsedSpace1, MaxSpace1, UsedSpace2, MaxSpace2, UsedSpace3, MaxSpace3 int 48 | } 49 | 50 | // +CMGL 51 | type MessageList []Message 52 | 53 | // Simple OK response 54 | type OK struct{} 55 | 56 | // Simple ERROR response 57 | type ERROR struct{} 58 | 59 | // Unknown 60 | type UnknownPacket struct { 61 | Command string 62 | Args []interface{} 63 | } 64 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package gogsmmodem 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "io" 12 | ) 13 | 14 | // Time format in AT protocol 15 | var TimeFormat = "06/01/02,15:04:05" 16 | 17 | // Parse an AT formatted time 18 | func parseTime(t string) time.Time { 19 | t = t[:len(t)-3] // ignore trailing +00 20 | ret, _ := time.Parse(TimeFormat, t) 21 | return ret 22 | } 23 | 24 | // Quote a value 25 | func quote(s interface{}) string { 26 | switch v := s.(type) { 27 | case string: 28 | if v == "?" { 29 | return v 30 | } 31 | return fmt.Sprintf(`"%s"`, v) 32 | case int, int64: 33 | return fmt.Sprint(v) 34 | default: 35 | panic(fmt.Sprintf("Unsupported argument type: %T", v)) 36 | } 37 | return "" 38 | } 39 | 40 | // Quote a list of values 41 | func quotes(args []interface{}) string { 42 | ret := make([]string, len(args)) 43 | for i, arg := range args { 44 | ret[i] = quote(arg) 45 | } 46 | return strings.Join(ret, ",") 47 | } 48 | 49 | // Check if s starts with p 50 | func startsWith(s, p string) bool { 51 | return strings.Index(s, p) == 0 52 | } 53 | 54 | // Unquote a string to a value (string or int) 55 | func unquote(s string) interface{} { 56 | if startsWith(s, `"`) { 57 | return strings.Trim(s, `"`) 58 | } 59 | if i, err := strconv.Atoi(s); err == nil { 60 | // number 61 | return i 62 | } 63 | return s 64 | } 65 | 66 | var RegexQuote = regexp.MustCompile(`"[^"]*"|[^,]*`) 67 | 68 | // Unquote a parameter list to values 69 | func unquotes(s string) []interface{} { 70 | vs := RegexQuote.FindAllString(s, -1) 71 | args := make([]interface{}, len(vs)) 72 | for i, v := range vs { 73 | args[i] = unquote(v) 74 | } 75 | return args 76 | } 77 | 78 | // Unquote a parameter list of strings 79 | func stringsUnquotes(s string) []string { 80 | args := unquotes(s) 81 | var res []string 82 | for _, arg := range args { 83 | res = append(res, fmt.Sprint(arg)) 84 | } 85 | return res 86 | } 87 | 88 | var gsm0338 map[rune]string = map[rune]string{ 89 | '@': "\x00", 90 | '£': "\x01", 91 | '$': "\x02", 92 | '¥': "\x03", 93 | 'è': "\x04", 94 | 'é': "\x05", 95 | 'ù': "\x06", 96 | 'ì': "\x07", 97 | 'ò': "\x08", 98 | 'Ç': "\x09", 99 | '\r': "\x0a", 100 | 'Ø': "\x0b", 101 | 'ø': "\x0c", 102 | '\n': "\x0d", 103 | 'Å': "\x0e", 104 | 'å': "\x0f", 105 | 'Δ': "\x10", 106 | '_': "\x11", 107 | 'Φ': "\x12", 108 | 'Γ': "\x13", 109 | 'Λ': "\x14", 110 | 'Ω': "\x15", 111 | 'Π': "\x16", 112 | 'Ψ': "\x17", 113 | 'Σ': "\x18", 114 | 'Θ': "\x19", 115 | 'Ξ': "\x1a", 116 | 'Æ': "\x1c", 117 | 'æ': "\x1d", 118 | 'É': "\x1f", 119 | ' ': " ", 120 | '!': "!", 121 | '"': "\"", 122 | '#': "#", 123 | '¤': "\x24", 124 | '%': "\x25", 125 | '&': "&", 126 | '\'': "'", 127 | '(': "(", 128 | ')': ")", 129 | '*': "*", 130 | '+': "+", 131 | ',': ",", 132 | '-': "-", 133 | '.': ".", 134 | '/': "/", 135 | '0': "0", 136 | '1': "1", 137 | '2': "2", 138 | '3': "3", 139 | '4': "4", 140 | '5': "5", 141 | '6': "6", 142 | '7': "7", 143 | '8': "8", 144 | '9': "9", 145 | ':': ":", 146 | ';': ";", 147 | '<': "<", 148 | '=': "=", 149 | '>': ">", 150 | '?': "?", 151 | '¡': "\x40", 152 | 'A': "A", 153 | 'B': "B", 154 | 'C': "C", 155 | 'D': "D", 156 | 'E': "E", 157 | 'F': "F", 158 | 'G': "G", 159 | 'H': "H", 160 | 'I': "I", 161 | 'J': "J", 162 | 'K': "K", 163 | 'L': "L", 164 | 'M': "M", 165 | 'N': "N", 166 | 'O': "O", 167 | 'P': "P", 168 | 'Q': "Q", 169 | 'R': "R", 170 | 'S': "S", 171 | 'T': "T", 172 | 'U': "U", 173 | 'V': "V", 174 | 'W': "W", 175 | 'X': "X", 176 | 'Y': "Y", 177 | 'Z': "Z", 178 | 'Ä': "\x5b", 179 | 'Ö': "\x5c", 180 | 'Ñ': "\x5d", 181 | 'Ü': "\x5e", 182 | '§': "\x5f", 183 | 'a': "a", 184 | 'b': "b", 185 | 'c': "c", 186 | 'd': "d", 187 | 'e': "e", 188 | 'f': "f", 189 | 'g': "g", 190 | 'h': "h", 191 | 'i': "i", 192 | 'j': "j", 193 | 'k': "k", 194 | 'l': "l", 195 | 'm': "m", 196 | 'n': "n", 197 | 'o': "o", 198 | 'p': "p", 199 | 'q': "q", 200 | 'r': "r", 201 | 's': "s", 202 | 't': "t", 203 | 'u': "u", 204 | 'v': "v", 205 | 'w': "w", 206 | 'x': "x", 207 | 'y': "y", 208 | 'z': "z", 209 | 'ä': "\x7b", 210 | 'ö': "\x7c", 211 | 'ñ': "\x7d", 212 | 'ü': "\x7e", 213 | 'à': "\x7f", 214 | // escaped characters 215 | '€': "\x1be", 216 | '[': "\x1b<", 217 | '\\': "\x1b/", 218 | ']': "\x1b>", 219 | '^': "\x1b^", 220 | '{': "\x1b(", 221 | '|': "\x1b@", 222 | '}': "\x1b)", 223 | '~': "\x1b=", 224 | } 225 | 226 | // Encode the string to GSM03.38 227 | func gsmEncode(s string) string { 228 | res := "" 229 | for _, c := range s { 230 | if d, ok := gsm0338[c]; ok { 231 | res += string(d) 232 | } 233 | } 234 | return res 235 | } 236 | 237 | // A logging ReadWriteCloser for debugging 238 | type LogReadWriteCloser struct { 239 | f io.ReadWriteCloser 240 | } 241 | 242 | func (self LogReadWriteCloser) Read(b []byte) (int, error) { 243 | n, err := self.f.Read(b) 244 | log.Printf("Read(%#v) = (%d, %v)\n", string(b[:n]), n, err) 245 | return n, err 246 | } 247 | 248 | func (self LogReadWriteCloser) Write(b []byte) (int, error) { 249 | n, err := self.f.Write(b) 250 | log.Printf("Write(%#v) = (%d, %v)\n", string(b), n, err) 251 | return n, err 252 | } 253 | 254 | func (self LogReadWriteCloser) Close() error { 255 | err := self.f.Close() 256 | log.Printf("Close() = %v\n", err) 257 | return err 258 | } 259 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package gogsmmodem 2 | 3 | import "fmt" 4 | 5 | func ExampleParseTime() { 6 | t := parseTime("14/02/01,15:07:43+00") 7 | fmt.Println(t) 8 | // Output: 9 | // 2014-02-01 15:07:43 +0000 UTC 10 | } 11 | 12 | func ExampleStartsWith() { 13 | fmt.Println(startsWith("abc", "ab")) 14 | fmt.Println(startsWith("abc", "b")) 15 | // Output: 16 | // true 17 | // false 18 | } 19 | 20 | func ExampleQuotes() { 21 | args := []interface{}{"a", 1, "b"} 22 | fmt.Println(quotes(args)) 23 | // Output: 24 | // "a",1,"b" 25 | } 26 | 27 | func ExampleUnquotes() { 28 | fmt.Println(unquotes(`"a,comma",1,"b"`)) 29 | // Output: 30 | // [a,comma 1 b] 31 | } 32 | 33 | func ExampleGsmEncode() { 34 | fmt.Printf("%q\n", gsmEncode("abcdefghijklmnopqrstuvwxyz")) 35 | fmt.Printf("%q\n", gsmEncode("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) 36 | fmt.Printf("%q\n", gsmEncode("0123456789")) 37 | fmt.Printf("%q\n", gsmEncode(".,+-*/ ")) 38 | fmt.Printf("%q\n", gsmEncode("°")) 39 | fmt.Printf("%q\n", gsmEncode("@£")) 40 | fmt.Printf("%q\n", gsmEncode("{}")) 41 | // Output: 42 | // "abcdefghijklmnopqrstuvwxyz" 43 | // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 44 | // "0123456789" 45 | // ".,+-*/ " 46 | // "" 47 | // "\x00\x01" 48 | // "\x1b(\x1b)" 49 | } 50 | --------------------------------------------------------------------------------