├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── at.go ├── calls └── calls.go ├── commands.go ├── doc.go ├── example └── daemon │ ├── README.md │ ├── main.go │ ├── monitor.go │ └── web.go ├── go.mod ├── go.sum ├── helper.go ├── integration_test.go ├── opts.go ├── opts_test.go ├── pdu ├── 7bit.go ├── 7bit_test.go ├── doc.go ├── semi_octet.go ├── semi_octet_test.go ├── ucs2.go └── usc2_test.go ├── sms ├── encoding.go ├── message_type.go ├── phone_number.go ├── phone_number_test.go ├── sms.go ├── sms_deliver.go ├── sms_status_report.go ├── sms_submit.go ├── sms_test.go ├── status.go ├── status_test.go ├── timestamp.go ├── timestamp_test.go ├── user_data_header.go ├── ussd.go └── validity_period.go └── util ├── util.go └── util_test.go /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | env: 11 | GOBIN: /tmp/.bin 12 | steps: 13 | - uses: actions/setup-go@v1 14 | with: 15 | go-version: "^1.x" 16 | 17 | - uses: actions/checkout@v2 18 | 19 | - uses: actions/cache@v1 20 | with: 21 | path: ~/go/pkg/mod 22 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 23 | 24 | - name: Run tests 25 | run: make test 26 | 27 | - uses: codecov/codecov-action@v2 28 | with: 29 | file: coverage.out 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | example/daemon/daemon 3 | coverage.out 4 | coverage.html 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2022 Maxim Kupriianov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the “Software”), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FILES_TO_FMT ?= $(shell find . -path ./vendor -prune -o -name '*.go' -print) 2 | 3 | .PHONY: help 4 | help: ## Print a short help message 5 | @grep -hE '^[a-zA-Z_-]+:[^:]*?## .*$$' $(MAKEFILE_LIST) | \ 6 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-18s\033[0m %s\n", $$1, $$2}' 7 | 8 | .PHONY: format 9 | format: ## Formats all Go code 10 | @echo ">> formatting code" 11 | @gofmt -s -w $(FILES_TO_FMT) 12 | 13 | .PHONY: lint 14 | lint: ## Runs golangci-lint on source files 15 | golangci-lint run 16 | 17 | .PHONY: coverage.out 18 | coverage.out: $(wildcard go.*) $(wildcard **/*.go) 19 | go test -race -covermode=atomic -coverprofile=$@ ./... 20 | 21 | coverage.html: coverage.out 22 | go tool cover -html $< -o $@ 23 | 24 | .PHONY: test 25 | test: coverage.out ## Run all Go tests (excluding integration tests) 26 | 27 | .PHONY: integration 28 | integration: ## Run Go tests (integration tests only) 29 | go test -race -tags=integration -covermode=atomic -coverprofile=coverage.out ./... 30 | 31 | .PHONY: clean 32 | clean: 33 | rm -rf coverage.* 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AT 2 | === 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/xlab/at.svg)](https://pkg.go.dev/github.com/xlab/at) 5 | [![Build Status](https://github.com/xlab/at/workflows/tests/badge.svg?branch=master)](https://github.com/xlab/at/actions) 6 | [![Coverage](https://codecov.io/gh/xlab/at/branch/master/graph/badge.svg)](https://codecov.io/gh/xlab/at) 7 | 8 | 9 | Package at is a framework for communication with AT-compatible devices like Huawei modems via serial port. Currently this package is well-suited for Huawei devices and since AT-commands set may vary from device to device, sometimes you'll be forced to implement some logic by yourself. 10 | 11 | ### Installation 12 | 13 | ``` 14 | go get github.com/xlab/at 15 | ``` 16 | 17 | Full documentation: [godoc](https://godoc.org/github.com/xlab/at). 18 | 19 | ### Features 20 | 21 | This framework includes facilities for device monitoring, sending and receiving AT-commands, encoding and decoding SMS messages from or to PDU octet representation (as specified in [3GPP TS 23.040]). An example of incoming SMS monitor application is given in [example/daemon]. 22 | 23 | [example/daemon]: https://github.com/xlab/at/blob/master/example/daemon 24 | [3GPP TS 23.040]: http://www.etsi.org/deliver/etsi_ts/123000_123099/123040/11.05.00_60/ts_123040v110500p.pdf 25 | 26 | ### Examples 27 | 28 | To get an SMS in a PDU octet representation: 29 | ```go 30 | smsSubmitGsm7 := Message{ 31 | Text: "hello world", 32 | Encoding: Encodings.Gsm7Bit, 33 | Type: MessageTypes.Submit, 34 | Address: "+79261234567", 35 | ServiceCenterAddress: "+79262000331", 36 | VP: ValidityPeriod(time.Hour * 24 * 4), 37 | VPFormat: ValidityPeriodFormats.Relative, 38 | } 39 | n, octets, err := smsSubmitGsm7.PDU() 40 | ``` 41 | 42 | To open a modem device: 43 | ```go 44 | dev = &Device{ 45 | CommandPort: CommandPortPath, 46 | NotifyPort: NotifyPortPath, 47 | } 48 | if err = dev.Open(); err != nil { 49 | return 50 | } 51 | ``` 52 | 53 | If you're going to use this framework and its methods instead of plain R/W you should initialize the modem beforehand: 54 | ``` 55 | if err = dev.Init(DeviceE173()); err != nil { 56 | return 57 | } 58 | ``` 59 | 60 | To use the wrapped version of a command: 61 | ```go 62 | err = dev.Commands.CUSD(UssdResultReporting.Enable, pdu.Encode7Bit(`*100#`), Encodings.Gsm7Bit) 63 | ``` 64 | 65 | Or to send a completely generic command: 66 | ```go 67 | str, err := dev.Send(`AT+GMM`) 68 | log.Println(str, err) 69 | ``` 70 | 71 | ### Device-specific config 72 | 73 | In order to introduce your own logic (i.e. custom modem Init function), you should derive your profile from the default DeviceProfile and override its methods. 74 | 75 | ### License 76 | 77 | [MIT](http://xlab.mit-license.org) 78 | -------------------------------------------------------------------------------- /at.go: -------------------------------------------------------------------------------- 1 | package at 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/xlab/at/calls" 11 | "github.com/xlab/at/pdu" 12 | "github.com/xlab/at/sms" 13 | ) 14 | 15 | // DefaultTimeout to close the connection in case of modem is being not responsive at all. 16 | const DefaultTimeout = time.Minute 17 | 18 | // sequence. 19 | const Sep = "\r\n" 20 | 21 | // Ctrl+Z code. 22 | const Sub = "\x1A" 23 | 24 | // Common errors. 25 | var ( 26 | ErrTimeout = errors.New("at: timeout") 27 | ErrUnknownEncoding = errors.New("at: unsupported encoding") 28 | ErrClosed = errors.New("at: device ports are closed") 29 | ErrNotInitialized = errors.New("at: not initialized") 30 | ErrWriteFailed = errors.New("at: write failed") 31 | ErrParseReport = errors.New("at: error while parsing report") 32 | ErrUnknownReport = errors.New("at: got unknown report") 33 | ) 34 | 35 | // Encoding is an encoding option to use. 36 | type Encoding byte 37 | 38 | // Encodings represents all the supported encodings. 39 | var Encodings = struct { 40 | Gsm7Bit Encoding 41 | UCS2 Encoding 42 | }{ 43 | 15, 72, 44 | } 45 | 46 | // Device represents a physical modem that supports Hayes AT-commands. 47 | type Device struct { 48 | // Name is the label to distinguish different devices. 49 | Name string 50 | // CommandPort is the path or name of command serial port. 51 | CommandPort string 52 | // CommandPort is the path or name of notification serial port. 53 | NotifyPort string 54 | // State holds the device state. 55 | State *DeviceState 56 | // Commands is a profile that provides implementation of Init and the other commands. 57 | Commands DeviceProfile 58 | // Timeout to override the default timeout (1m) 59 | Timeout time.Duration 60 | 61 | cmdPort *os.File 62 | notifyPort *os.File 63 | 64 | incomingCallerIDs chan *calls.CallerID 65 | messages chan *sms.Message 66 | ussd chan Ussd 67 | updated chan struct{} 68 | closed chan struct{} 69 | 70 | active bool 71 | } 72 | 73 | // IncomingCallerID fires when an incoming caller ID was received. 74 | func (d *Device) IncomingCallerID() <-chan *calls.CallerID { 75 | return d.incomingCallerIDs 76 | } 77 | 78 | // IncomingSms fires when an SMS was received. 79 | func (d *Device) IncomingSms() <-chan *sms.Message { 80 | return d.messages 81 | } 82 | 83 | // UssdReply fires when an Ussd reply was received. 84 | func (d *Device) UssdReply() <-chan Ussd { 85 | return d.ussd 86 | } 87 | 88 | // StateUpdate fires when DeviceState was updated by a received event. 89 | func (d *Device) StateUpdate() <-chan struct{} { 90 | return d.updated 91 | } 92 | 93 | // Closed fires when the connection was closed. 94 | func (d *Device) Closed() <-chan struct{} { 95 | return d.closed 96 | } 97 | 98 | // sendInteractive is a special case of Send, but this one is used whether 99 | // a prompt should be received first (i.e. when sending SMS, the PDU should be 100 | // entered after the device replied with '>') and then the second part of payload 101 | // should be sent (the second payload will be sent using Send). 102 | func (d *Device) sendInteractive(part1, part2 string, prompt byte) (reply string, err error) { 103 | 104 | err = d.withTimeout(func() error { 105 | _, err := d.cmdPort.Write([]byte(part1 + Sep)) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | // finally: send control character to exit interactive mode 111 | defer d.cmdPort.Write([]byte{pdu.Esc}) 112 | 113 | buf := bufio.NewReader(d.cmdPort) 114 | reply, err = buf.ReadString(prompt) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | reply, err = d.Send(part2 + Sub) 120 | return err 121 | }) 122 | 123 | return reply, err 124 | } 125 | 126 | // sanityCheck checks whether ports are opened and (if requested) that the initialization 127 | // was done. 128 | func (d *Device) sanityCheck(initialized bool) error { 129 | if d.cmdPort == nil { 130 | return ErrClosed 131 | } 132 | if d.notifyPort == nil { 133 | return ErrClosed 134 | } 135 | if initialized { 136 | if d.Commands == nil { 137 | return ErrNotInitialized 138 | } 139 | } 140 | return nil 141 | } 142 | 143 | // Send writes a command to the device's command port and parses the output. 144 | // Result will not contain any FinalReply since they're used to detect error status. 145 | // Multiple lines will be joined with '\n'. 146 | func (d *Device) Send(req string) (reply string, err error) { 147 | if err = d.sanityCheck(true); err != nil { 148 | return 149 | } 150 | 151 | err = d.withTimeout(func() error { 152 | _, err := d.cmdPort.Write([]byte(req + Sep)) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | var line string 158 | buf := bufio.NewReader(d.cmdPort) 159 | if line, err = buf.ReadString('\r'); err != nil { 160 | return err 161 | } 162 | text := strings.TrimSpace(line) 163 | if !strings.HasPrefix(req, text) { 164 | return err 165 | } 166 | 167 | var done bool 168 | for !done { 169 | if line, err = buf.ReadString('\r'); err != nil { 170 | break 171 | } 172 | text := strings.TrimSpace(line) 173 | if len(text) < 1 { 174 | continue 175 | } 176 | switch opt := FinalResults.Resolve(text); opt { 177 | case FinalResults.Ok, FinalResults.Noop: 178 | done = true 179 | case FinalResults.Timeout: 180 | err = ErrTimeout 181 | done = true 182 | case FinalResults.CmeError, FinalResults.CmsError: 183 | err = errors.New(text) 184 | done = true 185 | case FinalResults.Error, FinalResults.NotSupported, 186 | FinalResults.TooManyParameters, FinalResults.NoCarrier: 187 | err = errors.New(opt.Description) 188 | done = true 189 | default: 190 | if len(reply) > 0 { 191 | reply += "\n" 192 | } 193 | reply += text 194 | } 195 | } 196 | 197 | return err 198 | }) 199 | 200 | return 201 | } 202 | 203 | // runs the passed method with a timeout set on the cmdPort 204 | func (d *Device) withTimeout(f func() error) error { 205 | timeout := d.Timeout 206 | if timeout == 0 { 207 | timeout = DefaultTimeout 208 | } 209 | 210 | // enable deadline 211 | d.cmdPort.SetDeadline(time.Now().Add(timeout)) 212 | 213 | err := f() 214 | 215 | // disable deadline 216 | d.cmdPort.SetDeadline(time.Time{}) 217 | 218 | if err != nil && os.IsTimeout(err) { 219 | // reset connection on timeouts 220 | d.cmdPort.Write([]byte(KillCmd + Sep)) 221 | } 222 | 223 | return err 224 | } 225 | 226 | // Watch starts a monitoring process that will wait for events 227 | // from the device's notification port. 228 | func (d *Device) Watch() error { 229 | if d.notifyPort == nil { 230 | return errors.New("at: notification port not initialized") 231 | } 232 | go func() { 233 | <-d.closed 234 | d.notifyPort.Write([]byte(KillCmd + Sep)) 235 | }() 236 | 237 | buf := bufio.NewReader(d.notifyPort) 238 | for { 239 | select { 240 | case <-d.closed: 241 | return nil 242 | default: 243 | line, err := buf.ReadString(byte('\r')) 244 | if err != nil { 245 | d.Close() 246 | return nil 247 | } 248 | text := strings.TrimSpace(line) 249 | if len(text) < 1 { 250 | continue 251 | } 252 | d.handleReport(text) // ignore errors 253 | } 254 | } 255 | } 256 | 257 | // handleReport detects and parses a report from the notification port represented 258 | // as a string. The parsed values may change the inner state or be sent over out channels. 259 | func (d *Device) handleReport(str string) (err error) { 260 | report := Reports.Resolve(str) 261 | str = strings.TrimSpace(strings.TrimPrefix(str, report.ID)) 262 | switch report { 263 | case Reports.CallerID: 264 | var report callerIDReport 265 | if err = report.Parse(str); err != nil { 266 | return 267 | } 268 | 269 | callerID := report.GetCallerID() 270 | d.incomingCallerIDs <- callerID 271 | case Reports.Message: 272 | var report messageReport 273 | if err = report.Parse(str); err != nil { 274 | return 275 | } 276 | var octets []byte 277 | octets, err = d.Commands.CMGR(report.Index) 278 | if err != nil { 279 | return 280 | } 281 | if err = d.Commands.CMGD(report.Index, DeleteOptions.Index); err != nil { 282 | return 283 | } 284 | var msg sms.Message 285 | if _, err = msg.ReadFrom(octets); err != nil { 286 | return 287 | } 288 | d.messages <- &msg 289 | case Reports.Ussd: 290 | var ussd ussdReport 291 | if err = ussd.Parse(str); err != nil { 292 | return 293 | } 294 | var text string 295 | if ussd.Enc == Encodings.UCS2 { 296 | text, err = pdu.DecodeUcs2(ussd.Octets, false) 297 | if err != nil { 298 | return 299 | } 300 | } else if ussd.Enc == Encodings.Gsm7Bit { 301 | text, err = pdu.Decode7Bit(ussd.Octets) 302 | if err != nil { 303 | return 304 | } 305 | } else { 306 | return ErrUnknownEncoding 307 | } 308 | d.ussd <- Ussd(text) 309 | case Reports.SignalStrength: 310 | var rssi signalStrengthReport 311 | if err = rssi.Parse(str); err != nil { 312 | return 313 | } 314 | if d.State.SignalStrength != int(rssi) { 315 | d.State.SignalStrength = int(rssi) 316 | d.updated <- struct{}{} 317 | } 318 | case Reports.Mode: 319 | var report modeReport 320 | if err = report.Parse(str); err != nil { 321 | return 322 | } 323 | var updated bool 324 | if d.State.SystemMode != report.Mode { 325 | d.State.SystemMode = report.Mode 326 | updated = true 327 | } 328 | if d.State.SystemSubmode != report.Submode { 329 | d.State.SystemSubmode = report.Submode 330 | updated = true 331 | } 332 | if updated { 333 | d.updated <- struct{}{} 334 | } 335 | case Reports.ServiceState: 336 | var report serviceStateReport 337 | if err = report.Parse(str); err != nil { 338 | return 339 | } 340 | if d.State.ServiceState != Opt(report) { 341 | d.State.ServiceState = Opt(report) 342 | d.updated <- struct{}{} 343 | } 344 | case Reports.SimState: 345 | var report simStateReport 346 | if err = report.Parse(str); err != nil { 347 | return 348 | } 349 | if d.State.SimState != Opt(report) { 350 | d.State.SimState = Opt(report) 351 | d.updated <- struct{}{} 352 | } 353 | case Reports.BootHandshake: 354 | var token bootHandshakeReport 355 | if err = token.Parse(str); err != nil { 356 | return 357 | } 358 | if err = d.Commands.BOOT(uint64(token)); err != nil { 359 | return 360 | } 361 | case Reports.Stin: 362 | // ignore. what is this btw? 363 | default: 364 | switch FinalResults.Resolve(str) { 365 | case FinalResults.Noop, FinalResults.NotSupported, FinalResults.Timeout: 366 | // ignore 367 | default: 368 | return errors.New("at: unknown report: " + str) 369 | } 370 | } 371 | return nil 372 | } 373 | 374 | // Open is used to open serial ports of the device. This should be used first. 375 | // The method returns error if open was not succeed, i.e. if device is absent. 376 | func (d *Device) Open() (err error) { 377 | if d.cmdPort, err = os.OpenFile(d.CommandPort, os.O_RDWR, 0); err != nil { 378 | return 379 | } 380 | if d.NotifyPort != "" && d.NotifyPort != d.CommandPort { 381 | if d.notifyPort, err = os.OpenFile(d.NotifyPort, os.O_RDWR, 0); err != nil { 382 | d.cmdPort.Close() 383 | return 384 | } 385 | } 386 | return 387 | } 388 | 389 | // Init checks whether device is opened, initializes event channels 390 | // and runs init procedure defined within the supplied DeviceProfile. 391 | func (d *Device) Init(profile DeviceProfile) error { 392 | if err := d.sanityCheck(false); err != nil { 393 | return err 394 | } 395 | d.active = true 396 | d.closed = make(chan struct{}) 397 | d.incomingCallerIDs = make(chan *calls.CallerID, 100) 398 | d.messages = make(chan *sms.Message, 100) 399 | d.ussd = make(chan Ussd, 100) 400 | d.updated = make(chan struct{}, 100) 401 | d.Commands = profile 402 | return profile.Init(d) 403 | } 404 | 405 | // Close closes all the event channels and also closes 406 | // both command and notification modem's ports. This function may block 407 | // until the device will reply or the reply timeout timer will fire. 408 | // 409 | // Close is a no-op if already closed. 410 | func (d *Device) Close() (err error) { 411 | if d.active { 412 | d.active = false 413 | close(d.closed) 414 | } 415 | if d.cmdPort != nil { 416 | err = d.cmdPort.Close() 417 | } 418 | if d.notifyPort != nil { 419 | if err2 := d.notifyPort.Close(); err2 != nil { 420 | err = err2 421 | } 422 | } 423 | return 424 | } 425 | 426 | // SendUSSD sends an USSD request, the encoding and other parameters are default. 427 | func (d *Device) SendUSSD(req string) (err error) { 428 | err = d.Commands.CUSD(UssdResultReporting.Enable, pdu.Encode7Bit(req), Encodings.Gsm7Bit) 429 | return 430 | } 431 | 432 | // SendSMS sends an SMS message with given text to the given address, 433 | // the encoding and other parameters are default. 434 | func (d *Device) SendSMS(text string, address sms.PhoneNumber) (err error) { 435 | msg := sms.Message{ 436 | Text: text, 437 | Type: sms.MessageTypes.Submit, 438 | Encoding: sms.Encodings.Gsm7Bit, 439 | Address: address, 440 | VPFormat: sms.ValidityPeriodFormats.Relative, 441 | VP: sms.ValidityPeriod(24 * time.Hour * 4), 442 | } 443 | 444 | if !pdu.Is7BitEncodable(text) { 445 | msg.Encoding = sms.Encodings.UCS2 446 | } 447 | 448 | n, octets, err := msg.PDU() 449 | if err != nil { 450 | return 451 | } 452 | 453 | _, err = d.Commands.CMGS(n, octets) 454 | return 455 | } 456 | -------------------------------------------------------------------------------- /calls/calls.go: -------------------------------------------------------------------------------- 1 | package calls 2 | 3 | type CallerID struct { 4 | CallerID string 5 | IDType int 6 | IDValidity int 7 | } 8 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package at 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/xlab/at/calls" 9 | "github.com/xlab/at/pdu" 10 | "github.com/xlab/at/sms" 11 | "github.com/xlab/at/util" 12 | ) 13 | 14 | // DeviceProfile hides the device-specific implementation 15 | // and provides a set of methods that can be used on a device. 16 | // Init should be called first. 17 | type DeviceProfile interface { 18 | Init(*Device) error 19 | CMGS(length int, octets []byte) (byte, error) 20 | CUSD(reporting Opt, octets []byte, enc Encoding) (err error) 21 | CMGR(index uint16) (octets []byte, err error) 22 | CMGD(index uint16, option Opt) (err error) 23 | CMGL(flag Opt) (octets []MessageSlot, err error) 24 | CMGF(text bool) (err error) 25 | CLIP(text bool) (err error) 26 | CHUP() (err error) 27 | CNMI(mode, mt, bm, ds, bfr int) (err error) 28 | CPMS(mem1 StringOpt, mem2 StringOpt, mem3 StringOpt) (err error) 29 | BOOT(token uint64) (err error) 30 | SYSCFG(roaming, cellular bool) (err error) 31 | SYSINFO() (info *SystemInfoReport, err error) 32 | COPS(auto bool, text bool) (err error) 33 | OperatorName() (str string, err error) 34 | ModelName() (str string, err error) 35 | IMEI() (str string, err error) 36 | } 37 | 38 | // DeviceE173 returns an instance of DeviceProfile implementation for Huawei E173, 39 | // it's also the default one. 40 | func DeviceE173() DeviceProfile { 41 | return &DefaultProfile{} 42 | } 43 | 44 | // DefaultProfile is a reference implementation that could be embedded 45 | // in any other custom implementation of the DeviceProfile interface. 46 | type DefaultProfile struct { 47 | dev *Device 48 | DeviceProfile 49 | } 50 | 51 | // Init invokes a set of methods that will make the initial setup of the modem. 52 | func (p *DefaultProfile) Init(d *Device) (err error) { 53 | p.dev = d 54 | p.dev.Send(NoopCmd) // kinda flush 55 | if err = p.COPS(true, true); err != nil { 56 | return fmt.Errorf("at init: unable to adjust the format of operator's name: %w", err) 57 | } 58 | var info *SystemInfoReport 59 | if info, err = p.SYSINFO(); err != nil { 60 | return fmt.Errorf("at init: unable to read system info: %w", err) 61 | } 62 | p.dev.State = &DeviceState{ 63 | ServiceState: info.ServiceState, 64 | ServiceDomain: info.ServiceDomain, 65 | RoamingState: info.RoamingState, 66 | SystemMode: info.SystemMode, 67 | SystemSubmode: info.SystemSubmode, 68 | SimState: info.SimState, 69 | } 70 | if p.dev.State.OperatorName, err = p.OperatorName(); err != nil { 71 | return fmt.Errorf("at init: unable to read operator's name: %w", err) 72 | } 73 | if p.dev.State.ModelName, err = p.ModelName(); err != nil { 74 | return fmt.Errorf("at init: unable to read modem's model name: %w", err) 75 | } 76 | if p.dev.State.IMEI, err = p.IMEI(); err != nil { 77 | return fmt.Errorf("at init: unable to read modem's IMEI code: %w", err) 78 | } 79 | if err = p.CMGF(false); err != nil { 80 | return fmt.Errorf("at init: unable to switch message format to PDU: %w", err) 81 | } 82 | if err = p.CPMS(MemoryTypes.NvRAM, MemoryTypes.NvRAM, MemoryTypes.NvRAM); err != nil { 83 | return fmt.Errorf("at init: unable to set messages storage: %w", err) 84 | } 85 | if err = p.CNMI(1, 1, 0, 0, 0); err != nil { 86 | return fmt.Errorf("at init: unable to turn on message notifications: %w", err) 87 | } 88 | if err = p.CLIP(true); err != nil { 89 | return fmt.Errorf("at init: unable to turn on calling party ID notifications: %w", err) 90 | } 91 | 92 | return p.FetchInbox() 93 | } 94 | 95 | func (p *DefaultProfile) FetchInbox() error { 96 | slots, err := p.CMGL(MessageFlags.Any) 97 | if err != nil { 98 | return fmt.Errorf("unable to check message inbox: %w", err) 99 | } 100 | 101 | for i := range slots { 102 | var msg sms.Message 103 | if _, err := msg.ReadFrom(slots[i].Payload); err != nil { 104 | return fmt.Errorf("error while parsing message inbox: %w", err) 105 | } 106 | if err := p.CMGD(slots[i].Index, DeleteOptions.Index); err != nil { 107 | return fmt.Errorf("error while cleaning message inbox: %w", err) 108 | } 109 | p.dev.messages <- &msg 110 | } 111 | return nil 112 | } 113 | 114 | type signalStrengthReport uint64 115 | 116 | func (s *signalStrengthReport) Parse(str string) error { 117 | u, err := parseUint8(str) 118 | *s = signalStrengthReport(u) 119 | return err 120 | } 121 | 122 | type modeReport struct { 123 | Mode Opt 124 | Submode Opt 125 | } 126 | 127 | func (m *modeReport) Parse(str string) (err error) { 128 | fields := strings.Split(str, ",") 129 | if len(fields) < 2 { 130 | return ErrParseReport 131 | } 132 | 133 | mode, err := parseUint8(fields[0]) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | submode, err := parseUint8(fields[1]) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | m.Mode = SystemModes.Resolve(int(mode)) 144 | m.Submode = SystemSubmodes.Resolve(int(submode)) 145 | return 146 | } 147 | 148 | type simStateReport Opt 149 | 150 | func (s *simStateReport) Parse(str string) (err error) { 151 | o, err := parseUint8(str) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | *s = simStateReport(SimStates.Resolve(int(o))) 157 | return nil 158 | } 159 | 160 | type serviceStateReport Opt 161 | 162 | func (s *serviceStateReport) Parse(str string) error { 163 | i, err := parseUint8(str) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | *s = serviceStateReport(ServiceStates.Resolve(int(i))) 169 | return nil 170 | } 171 | 172 | type bootHandshakeReport uint64 173 | 174 | func (b *bootHandshakeReport) Parse(str string) error { 175 | fields := strings.Split(str, ",") 176 | if len(fields) < 1 { 177 | return ErrParseReport 178 | } 179 | 180 | key, err := parseUint8(fields[0]) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | *b = bootHandshakeReport(key) 186 | return nil 187 | } 188 | 189 | // Ussd type represents the USSD query string. 190 | type Ussd string 191 | 192 | // Encode converts the query string into bytes according to the 193 | // specified encoding. 194 | func (u *Ussd) Encode(enc Encoding) ([]byte, error) { 195 | switch enc { 196 | case Encodings.Gsm7Bit: 197 | return pdu.Encode7Bit(u.String()), nil 198 | case Encodings.UCS2: 199 | return pdu.EncodeUcs2(u.String()), nil 200 | default: 201 | return nil, ErrUnknownEncoding 202 | } 203 | } 204 | 205 | func (u *Ussd) String() string { 206 | return string(*u) 207 | } 208 | 209 | type ussdReport struct { 210 | N uint8 211 | Octets []byte 212 | Enc Encoding 213 | } 214 | 215 | func (r *ussdReport) Parse(str string) (err error) { 216 | fields := strings.Split(str, ",") 217 | if len(fields) < 3 { 218 | return ErrParseReport 219 | } 220 | if r.N, err = parseUint8(fields[0]); err != nil { 221 | return 222 | } 223 | if r.Octets, err = util.Bytes(strings.Trim(fields[1], `"`)); err != nil { 224 | return 225 | } 226 | var e uint8 227 | if e, err = parseUint8(fields[2]); err != nil { 228 | return 229 | } 230 | r.Enc = Encoding(e) 231 | return 232 | } 233 | 234 | // CUSD sends AT+CUSD with the given parameters to the device. This will invoke an USSD request. 235 | func (p *DefaultProfile) CUSD(reporting Opt, octets []byte, enc Encoding) (err error) { 236 | req := fmt.Sprintf(`AT+CUSD=%d,%02X,%d`, reporting.ID, octets, enc) 237 | _, err = p.dev.Send(req) 238 | return 239 | } 240 | 241 | type callerIDReport struct { 242 | CallerID string 243 | IDType Opt 244 | IDValidity Opt 245 | } 246 | 247 | func (c *callerIDReport) Parse(str string) (err error) { 248 | fields := strings.Split(str, ",") 249 | if len(fields) != 6 { 250 | return ErrParseReport 251 | } 252 | 253 | c.CallerID = strings.Trim(fields[0], "\"") 254 | 255 | var t uint8 256 | if t, err = parseUint8(fields[1]); err != nil { 257 | return 258 | } 259 | c.IDType = CallerIDTypes.Resolve(int(t)) 260 | 261 | var v uint8 262 | if v, err = parseUint8(fields[5]); err != nil { 263 | return 264 | } 265 | c.IDType = CallerIDTypes.Resolve(int(v)) 266 | 267 | return nil 268 | } 269 | 270 | func (c *callerIDReport) GetCallerID() *calls.CallerID { 271 | return &calls.CallerID{ 272 | CallerID: c.CallerID, 273 | IDType: c.IDType.ID, 274 | IDValidity: c.IDValidity.ID, 275 | } 276 | } 277 | 278 | type messageReport struct { 279 | Memory StringOpt 280 | Index uint16 281 | } 282 | 283 | func (m *messageReport) Parse(str string) (err error) { 284 | fields := strings.Split(str, ",") 285 | if len(fields) < 2 { 286 | return ErrParseReport 287 | } 288 | if m.Memory = MemoryTypes.Resolve(strings.Trim(fields[0], `"`)); m.Memory == UnknownStringOpt { 289 | return ErrParseReport 290 | } 291 | if m.Index, err = parseUint16(fields[1]); err != nil { 292 | return 293 | } 294 | return 295 | } 296 | 297 | // CMGR sends AT+CMGR with the given index to the device and returns the message contents. 298 | func (p *DefaultProfile) CMGR(index uint16) (octets []byte, err error) { 299 | req := fmt.Sprintf(`AT+CMGR=%d`, index) 300 | reply, err := p.dev.Send(req) 301 | if err != nil { 302 | return 303 | } 304 | lines := strings.Split(reply, "\n") 305 | if len(lines) < 2 { 306 | return nil, ErrParseReport 307 | } 308 | octets, err = util.Bytes(lines[1]) 309 | return 310 | } 311 | 312 | // CMGD sends AT+CMGD with the given index and option to the device. Option defines the mode 313 | // in which messages will be deleted. The default mode is to delete by index. 314 | func (p *DefaultProfile) CMGD(index uint16, option Opt) (err error) { 315 | req := fmt.Sprintf(`AT+CMGD=%d,%d`, index, option.ID) 316 | _, err = p.dev.Send(req) 317 | return 318 | } 319 | 320 | // CPMS sends AT+CPMS with the given options to the device. It allows to select 321 | // the storage type for different kinds of messages and message notifications. 322 | func (p *DefaultProfile) CPMS(mem1 StringOpt, mem2 StringOpt, mem3 StringOpt) (err error) { 323 | req := fmt.Sprintf(`AT+CPMS="%s","%s","%s"`, mem1.ID, mem2.ID, mem3.ID) 324 | _, err = p.dev.Send(req) 325 | return 326 | } 327 | 328 | // CNMI sends AT+CNMI with the given parameters to the device. 329 | // It's used to adjust the settings of the new message arrival notifications. 330 | func (p *DefaultProfile) CNMI(mode, mt, bm, ds, bfr int) (err error) { 331 | req := fmt.Sprintf(`AT+CNMI=%d,%d,%d,%d,%d`, mode, mt, bm, ds, bfr) 332 | _, err = p.dev.Send(req) 333 | return 334 | } 335 | 336 | // CMGF sends AT+CMGF with the given value to the device. It toggles 337 | // the mode of message handling between PDU and TEXT. 338 | // 339 | // Note, that the at package works only in PDU mode. 340 | func (p *DefaultProfile) CMGF(text bool) (err error) { 341 | var flag int 342 | if text { 343 | flag = 1 344 | } 345 | req := fmt.Sprintf(`AT+CMGF=%d`, flag) 346 | _, err = p.dev.Send(req) 347 | return 348 | } 349 | 350 | // CLIP sends AT+CLIP with the given value to the device. It toggles 351 | // the mode of periodic calling party ID notification 352 | func (p *DefaultProfile) CLIP(text bool) (err error) { 353 | var flag int 354 | if text { 355 | flag = 1 356 | } 357 | req := fmt.Sprintf(`AT+CLIP=%d`, flag) 358 | _, err = p.dev.Send(req) 359 | return 360 | } 361 | 362 | // CHUP sends ATH+CHUP to the device. It hangs up 363 | // an active incoming call 364 | func (p *DefaultProfile) CHUP() (err error) { 365 | req := "ATH+CHUP" 366 | _, err = p.dev.Send(req) 367 | return 368 | } 369 | 370 | type MessageSlot struct { 371 | Index uint16 372 | Payload []byte 373 | } 374 | 375 | // CMGL sends AT+CMGL with the given filtering flag to the device and then parses 376 | // the list of received messages that match their filter. See MessageFlags for the 377 | // list of supported filters. 378 | func (p *DefaultProfile) CMGL(flag Opt) (result []MessageSlot, err error) { 379 | req := fmt.Sprintf(`AT+CMGL=%d`, flag.ID) 380 | reply, err := p.dev.Send(req) 381 | if err != nil { 382 | return 383 | } 384 | lines := strings.Split(reply, "\n") 385 | if len(lines) < 2 { 386 | return 387 | } 388 | 389 | for i := 0; i < len(lines); i += 2 { 390 | header := strings.TrimPrefix(lines[i], `+CMGL: `) 391 | fields := strings.Split(header, ",") 392 | if len(fields) < 4 { 393 | return nil, ErrParseReport 394 | } 395 | n, err := parseUint16(fields[0]) 396 | if err != nil { 397 | return nil, ErrParseReport 398 | } 399 | var oct []byte 400 | if oct, err = util.Bytes(lines[i+1]); err != nil { 401 | return nil, ErrParseReport 402 | } 403 | 404 | result = append(result, MessageSlot{ 405 | Index: n, 406 | Payload: oct, 407 | }) 408 | } 409 | return 410 | } 411 | 412 | // BOOT sends AT^BOOT with the given token to the device. This completes 413 | // the handshaking procedure. 414 | func (p *DefaultProfile) BOOT(token uint64) (err error) { 415 | req := fmt.Sprintf(`AT^BOOT=%d,0`, token) 416 | _, err = p.dev.Send(req) 417 | return 418 | } 419 | 420 | // CMGS sends AT+CMGS with the given parameters to the device. This is used to send SMS 421 | // using the given PDU data. Length is a number of TPDU bytes. 422 | // Returns the reference number of the sent message. 423 | func (p *DefaultProfile) CMGS(length int, octets []byte) (byte, error) { 424 | part1 := fmt.Sprintf("AT+CMGS=%d", length) 425 | part2 := fmt.Sprintf("%02X", octets) 426 | reply, err := p.dev.sendInteractive(part1, part2, byte('>')) 427 | 428 | if err != nil { 429 | return 0, err 430 | } 431 | 432 | if !strings.HasPrefix(reply, "+CMGS: ") { 433 | return 0, fmt.Errorf("unable to get sequence number of reply '%s'", reply) 434 | } 435 | 436 | number, err := parseUint8(reply[7:]) 437 | if err != nil { 438 | return 0, fmt.Errorf("unable to parse sequence number of reply '%s': %w", reply, err) 439 | } 440 | 441 | return byte(number), nil 442 | } 443 | 444 | // SYSCFG sends AT^SYSCFG with the given parameters to the device. 445 | // The arguments of this command may vary, so the options are limited to switchng roaming and 446 | // cellular mode on/off. 447 | func (p *DefaultProfile) SYSCFG(roaming, cellular bool) (err error) { 448 | var roam int 449 | if roaming { 450 | roam = 1 451 | } 452 | var cell int 453 | if cellular { 454 | cell = 2 455 | } else { 456 | cell = 1 457 | } 458 | req := fmt.Sprintf(`AT^SYSCFG=2,2,3FFFFFFF,%d,%d`, roam, cell) 459 | _, err = p.dev.Send(req) 460 | return 461 | } 462 | 463 | // SystemInfoReport represents the report from the AT^SYSINFO command. 464 | type SystemInfoReport struct { 465 | ServiceState Opt 466 | ServiceDomain Opt 467 | RoamingState Opt 468 | SystemMode Opt 469 | SystemSubmode Opt 470 | SimState Opt 471 | } 472 | 473 | // Parse scans the AT^SYSINFO report into a non-nil SystemInfoReport struct. 474 | func (s *SystemInfoReport) Parse(str string) (err error) { 475 | fields := strings.Split(str, ",") 476 | if len(fields) < 7 { 477 | return ErrParseReport 478 | } 479 | 480 | fetch := func(str string, field *Opt, resolver func(id int) Opt) error { 481 | if n, err := parseUint8(str); err != nil { 482 | return err 483 | } else if opt := resolver(int(n)); opt == UnknownOpt { 484 | return errors.New("resolver: unknown opt") 485 | } else { 486 | *field = opt 487 | return nil 488 | } 489 | } 490 | 491 | if err = fetch(fields[0], &s.ServiceState, ServiceStates.Resolve); err != nil { 492 | return ErrParseReport 493 | } 494 | if err = fetch(fields[1], &s.ServiceDomain, ServiceDomains.Resolve); err != nil { 495 | return ErrParseReport 496 | } 497 | if err = fetch(fields[2], &s.RoamingState, RoamingStates.Resolve); err != nil { 498 | return ErrParseReport 499 | } 500 | if err = fetch(fields[3], &s.SystemMode, SystemModes.Resolve); err != nil { 501 | return ErrParseReport 502 | } 503 | if err = fetch(fields[4], &s.SimState, SimStates.Resolve); err != nil { 504 | return ErrParseReport 505 | } 506 | if err = fetch(fields[6], &s.SystemSubmode, SystemSubmodes.Resolve); err != nil { 507 | return ErrParseReport 508 | } 509 | return nil 510 | } 511 | 512 | // SYSINFO sends AT^SYSINFO to the device and parses the output. 513 | func (p *DefaultProfile) SYSINFO() (info *SystemInfoReport, err error) { 514 | reply, err := p.dev.Send(`AT^SYSINFO`) 515 | if err != nil { 516 | return nil, err 517 | } 518 | info = new(SystemInfoReport) 519 | err = info.Parse(strings.TrimPrefix(reply, `^SYSINFO:`)) 520 | return 521 | } 522 | 523 | // COPS sends AT+COPS to the device with parameters that define autosearch and 524 | // the operator's name representation. The default representation is numerical. 525 | func (p *DefaultProfile) COPS(auto bool, text bool) (err error) { 526 | var a, t int 527 | if !auto { 528 | a = 1 529 | } 530 | if !text { 531 | t = 2 532 | } 533 | req := fmt.Sprintf(`AT+COPS=%d,%d`, a, t) 534 | _, err = p.dev.Send(req) 535 | return 536 | } 537 | 538 | // OperatorName sends AT+COPS? to the device and gets the operator's name. 539 | func (p *DefaultProfile) OperatorName() (str string, err error) { 540 | result, err := p.dev.Send(`AT+COPS?`) 541 | fields := strings.Split(strings.TrimPrefix(result, `+COPS: `), ",") 542 | if len(fields) < 4 { 543 | err = ErrParseReport 544 | return 545 | } 546 | str = strings.TrimLeft(strings.TrimRight(fields[2], `"`), `"`) 547 | return 548 | } 549 | 550 | // ModelName sends AT+GMM to the device and gets the modem's model name. 551 | func (p *DefaultProfile) ModelName() (str string, err error) { 552 | str, err = p.dev.Send(`AT+GMM`) 553 | return 554 | } 555 | 556 | // IMEI sends AT+GSN to the device and gets the modem's IMEI code. 557 | func (p *DefaultProfile) IMEI() (str string, err error) { 558 | str, err = p.dev.Send(`AT+GSN`) 559 | return 560 | } 561 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package at is a framework for communication with AT-compatible devices 2 | // like Huawei modems via serial port. Currently this package is well-suited 3 | // for Huawei devices and since AT-commands set may vary from device to 4 | // device, sometimes you'll be forced to implement some logic by yourself. 5 | // 6 | // Framework 7 | // 8 | // This framework includes facilities for device monitoring, sending and 9 | // receiving AT-commands, encoding and decoding SMS messages from or to PDU 10 | // octet representation (as specified in 3GPP TS 23.040). An example of 11 | // incoming SMS monitor application is given. 12 | // 13 | // Device-specific config 14 | // 15 | // In order to introduce your own logic (i.e. custom modem Init function), 16 | // you should derive your profile from the default DeviceProfile and 17 | // override its methods. 18 | // 19 | // About 20 | // 21 | // Project page: https://github.com/xlab/at 22 | package at 23 | -------------------------------------------------------------------------------- /example/daemon/README.md: -------------------------------------------------------------------------------- 1 | ## AT / example / daemon 2 | 3 | This utility should be used as an example of the 'at' package usage. The daemon waits for a modem device that is specified in constants. 4 | 5 | ```go 6 | CommandPortPath = "/dev/tty.HUAWEIMobile-Modem" 7 | NotifyPortPath = "/dev/tty.HUAWEIMobile-Pcui" 8 | ``` 9 | 10 | And monitors its SMS inbox and balance. 11 | 12 | ```go 13 | BalanceUSSD = "*100#" 14 | BalanceCheckInterval = time.Minute 15 | DeviceCheckInterval = time.Second * 10 16 | ``` 17 | 18 | It also spawns a web interface available at `http://localhost:%d`. 19 | 20 | [Screenshot](http://cl.ly/XPuS/Image%202014-09-07%20at%207.51.48%20pm.png) -------------------------------------------------------------------------------- /example/daemon/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "log" 4 | 5 | const ( 6 | CommandPortPath = "/dev/tty.HUAWEIMobile-Modem" 7 | NotifyPortPath = "/dev/tty.HUAWEIMobile-Pcui" 8 | WebPort = 5051 9 | ) 10 | 11 | func main() { 12 | log.Printf("Staring daemon at http://localhost:%d", WebPort) 13 | mon := NewMonitor(CommandPortPath, NotifyPortPath) 14 | 15 | if err := mon.Run(); err != nil { 16 | log.Fatalln(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/daemon/monitor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/xlab/at" 10 | "github.com/xlab/at/sms" 11 | ) 12 | 13 | const ( 14 | BalanceUSSD = "*100#" 15 | BalanceCheckInterval = time.Minute 16 | DeviceCheckInterval = time.Second * 10 17 | ) 18 | 19 | type State uint8 20 | 21 | const ( 22 | NoDeviceState State = iota 23 | ReadyState 24 | ) 25 | 26 | type Monitor struct { 27 | // Messages is the most simpliest volatile DB storing sms messages. 28 | Messages []*sms.Message 29 | // Balance is the balance reply we've got with an USSD query. 30 | Balance string 31 | // Ready signals if device is ready. 32 | Ready bool 33 | 34 | cmdPort string 35 | notifyPort string 36 | 37 | dev *at.Device 38 | stateChanged chan State 39 | checkTimer *time.Timer 40 | } 41 | 42 | func (m *Monitor) DeviceState() *at.DeviceState { 43 | return m.dev.State 44 | } 45 | 46 | func NewMonitor(cmdPort, notifyPort string) *Monitor { 47 | return &Monitor{ 48 | cmdPort: cmdPort, 49 | notifyPort: notifyPort, 50 | stateChanged: make(chan State, 10), 51 | } 52 | } 53 | 54 | func (m *Monitor) devStop() { 55 | if m.dev != nil { 56 | m.dev.Close() 57 | } 58 | } 59 | 60 | func (m *Monitor) Run() (err error) { 61 | m.checkTimer = time.NewTimer(DeviceCheckInterval) 62 | defer m.checkTimer.Stop() 63 | defer m.devStop() 64 | 65 | go func() { 66 | for { 67 | <-m.checkTimer.C 68 | if err := m.openDevice(); err != nil { 69 | m.checkTimer.Reset(DeviceCheckInterval) 70 | continue 71 | } else { 72 | m.checkTimer.Stop() 73 | m.stateChanged <- ReadyState 74 | } 75 | } 76 | }() 77 | 78 | if err := m.openDevice(); err != nil { 79 | m.stateChanged <- NoDeviceState 80 | } else { 81 | m.stateChanged <- ReadyState 82 | m.checkTimer.Stop() 83 | } 84 | 85 | go func() { 86 | for s := range m.stateChanged { 87 | switch s { 88 | case NoDeviceState: 89 | m.Balance = "" 90 | m.Ready = false 91 | log.Println("Waiting for device") 92 | m.checkTimer.Reset(DeviceCheckInterval) 93 | case ReadyState: 94 | log.Println("Device connected") 95 | m.Ready = true 96 | go func() { 97 | m.dev.Watch() 98 | m.stateChanged <- NoDeviceState 99 | }() 100 | go func() { 101 | m.dev.SendUSSD(BalanceUSSD) 102 | t := time.NewTicker(BalanceCheckInterval) 103 | defer t.Stop() 104 | for { 105 | select { 106 | case <-m.dev.Closed(): 107 | return 108 | case ussd, ok := <-m.dev.UssdReply(): 109 | if ok { 110 | m.Balance = string(ussd) 111 | } 112 | case msg, ok := <-m.dev.IncomingSms(): 113 | if ok { 114 | m.Messages = append(m.Messages, msg) 115 | } 116 | case <-t.C: 117 | m.dev.SendUSSD(BalanceUSSD) 118 | } 119 | } 120 | }() 121 | } 122 | } 123 | }() 124 | 125 | return http.ListenAndServe(":"+strconv.Itoa(WebPort), m) 126 | } 127 | 128 | func (m *Monitor) openDevice() (err error) { 129 | m.dev = &at.Device{ 130 | CommandPort: m.cmdPort, 131 | NotifyPort: m.notifyPort, 132 | } 133 | if err = m.dev.Open(); err != nil { 134 | return 135 | } 136 | if err = m.dev.Init(at.DeviceE173()); err != nil { 137 | return 138 | } 139 | return 140 | } 141 | -------------------------------------------------------------------------------- /example/daemon/web.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "io" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/xlab/at" 12 | "github.com/xlab/at/sms" 13 | ) 14 | 15 | func (m *Monitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 16 | data := struct { 17 | Mon *Monitor 18 | Dev *at.Device 19 | Time time.Time 20 | }{ 21 | Mon: m, 22 | Dev: m.dev, 23 | Time: time.Now(), 24 | } 25 | 26 | var buf bytes.Buffer 27 | err := tpl.Execute(&buf, data) 28 | if err != nil { 29 | http.Error(w, err.Error(), http.StatusInternalServerError) 30 | } 31 | io.Copy(w, &buf) 32 | } 33 | 34 | func decorateSignalStrength(n int) string { 35 | switch { 36 | case n == 0: 37 | return "< -113 dBm" 38 | case n == 1: 39 | return "-111 dBm" 40 | case n >= 2 && n <= 30: 41 | return fmt.Sprintf("%d dBm", -49-2*n) 42 | case n == 31: 43 | return "> -51 dBm" 44 | default: 45 | return "-" 46 | } 47 | } 48 | 49 | func decorateTime(t time.Time) string { 50 | return t.Format(time.RFC1123) 51 | } 52 | 53 | func decorateTimestamp(t sms.Timestamp) string { 54 | return time.Time(t).Format(time.RFC1123) 55 | } 56 | 57 | func inc(i int) int { 58 | return i + 1 59 | } 60 | 61 | var fm = template.FuncMap{ 62 | "time": decorateTime, 63 | "timestamp": decorateTimestamp, 64 | "signalStrength": decorateSignalStrength, 65 | "inc": inc, 66 | } 67 | 68 | var tpl = template.Must(template.New("index.html").Funcs(fm).Parse(indexTpl)) 69 | 70 | const indexTpl = ` 71 | 72 | 73 | 74 | 75 | {{ with .Dev.State }}{{ .ModelName }} {{ end }}Status 76 | 77 | 78 | 79 | 80 |
81 | 86 |
87 | {{ if .Mon.Ready }} 88 |
89 |

Operator

90 |

{{ .Dev.State.OperatorName }}

91 |

Signal strength

92 |

{{ signalStrength .Dev.State.SignalStrength }}

93 |

Network mode

94 |

{{ .Dev.State.SystemSubmode.Description }}

95 |
96 |
97 |

Balance

98 |

{{with .Mon.Balance}}{{.}}{{ else }}-{{end}}

99 |

Received messages

100 |

{{ len .Mon.Messages }}

101 |

Last update

102 |

{{ time .Time }}

103 |
104 | {{ else }} 105 |
106 |

Status

107 |

Disconnected

108 |
109 |
110 |

Last update

111 |

{{ time .Time }}

112 |
113 | {{ end }} 114 |
115 |

Inbox

116 | 117 | {{ range $k,$v := .Mon.Messages }} 118 | 119 | 120 | 121 | 122 | 123 | 124 | {{ else }} 125 | 126 | 129 | 130 | {{ end }} 131 |
{{ inc $k }}{{ timestamp $v.ServiceCenterTime }}{{ $v.Address }}{{ $v.Text }}
127 |

Empty

128 |
132 |
133 | 134 | 135 | ` 136 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xlab/at 2 | 3 | go 1.17 4 | 5 | require github.com/stretchr/testify v1.7.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.0 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package at 2 | 3 | import "strconv" 4 | 5 | func parseUint8(str string) (uint8, error) { 6 | i, err := strconv.ParseUint(str, 10, 8) 7 | return uint8(i), err 8 | } 9 | 10 | func parseUint16(str string) (uint16, error) { 11 | i, err := strconv.ParseUint(str, 10, 16) 12 | return uint16(i), err 13 | } 14 | -------------------------------------------------------------------------------- /integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package at 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/xlab/at/pdu" 11 | ) 12 | 13 | // Needs to be changed for each particular configuration. 14 | const ( 15 | CommandPortPath = "/dev/tty.HUAWEIMobile-Modem" 16 | NotifyPortPath = "/dev/tty.HUAWEIMobile-Pcui" 17 | TestPhoneAddress = "+79269965690" 18 | BalanceUSSD = "*100#" 19 | ) 20 | 21 | var dev *Device 22 | 23 | // openDevice opens the hardcoded device paths for reading and writing, 24 | // also inits this device with the default device profile. 25 | func openDevice() (err error) { 26 | dev = &Device{ 27 | CommandPort: CommandPortPath, 28 | NotifyPort: NotifyPortPath, 29 | } 30 | if err = dev.Open(); err != nil { 31 | return 32 | } 33 | if err = dev.Init(DeviceE173()); err != nil { 34 | return 35 | } 36 | return 37 | } 38 | 39 | // waitDevice monitors available channels for the given period of time, or 40 | // until the fetch process exits. 41 | func waitDevice(n int) { 42 | t := time.NewTimer(time.Second * time.Duration(n)) 43 | defer t.Stop() 44 | go dev.Watch() 45 | for { 46 | select { 47 | case <-t.C: 48 | return 49 | case <-dev.Closed(): 50 | return 51 | case msg, ok := <-dev.IncomingSms(): 52 | if ok { 53 | log.Printf("Incoming sms from %s: %s", msg.Address, msg.Text) 54 | } 55 | case ussd, ok := <-dev.UssdReply(): 56 | if ok { 57 | log.Printf("USSD result: %s", ussd) 58 | } 59 | case <-dev.StateUpdate(): 60 | log.Printf("Signal strength: %d (%s/%s)", dev.State.SignalStrength, dev.State.OperatorName, 61 | dev.State.SystemSubmode.Description) 62 | } 63 | } 64 | } 65 | 66 | // This costs money (but works) 67 | 68 | // func TestSmsSend(t *testing.T) { 69 | // err := openDevice() 70 | // require.NoError(t, err) 71 | // defer dev.Close() 72 | // 73 | // msg := sms.Message{ 74 | // Text: "Lazy fox jumps over ленивая собака", 75 | // Type: sms.MessageTypes.Submit, 76 | // Encoding: sms.Encodings.UCS2, 77 | // Address: sms.PhoneNumber(TestPhoneAddress), 78 | // VPFormat: sms.ValidityPeriodFormats.Relative, 79 | // VP: sms.ValidityPeriod(24 * time.Hour * 4), 80 | // } 81 | // n, octets, err := msg.PDU() 82 | // require.NoError(t, err) 83 | // 84 | // err = dev.Commands.CMGS(n, octets) 85 | // require.NoError(t, err) 86 | // waitDevice(10) 87 | // } 88 | 89 | // Test the device lifecycle. 90 | func TestOpenInitWaitClose(t *testing.T) { 91 | err := openDevice() 92 | require.NoError(t, err) 93 | defer dev.Close() 94 | waitDevice(1) 95 | } 96 | 97 | // Test the "AT" command. 98 | func TestNoop(t *testing.T) { 99 | err := openDevice() 100 | require.NoError(t, err) 101 | defer dev.Close() 102 | _, err = dev.Send(NoopCmd) 103 | require.NoError(t, err) 104 | } 105 | 106 | // Test USSD queries, the result will be reported asynchroniously. 107 | func TestUssd(t *testing.T) { 108 | err := openDevice() 109 | require.NoError(t, err) 110 | defer dev.Close() 111 | err = dev.Commands.CUSD(UssdResultReporting.Enable, pdu.Encode7Bit(BalanceUSSD), Encodings.Gsm7Bit) 112 | require.NoError(t, err) 113 | waitDevice(10) 114 | } 115 | -------------------------------------------------------------------------------- /opts.go: -------------------------------------------------------------------------------- 1 | package at 2 | 3 | import "strings" 4 | 5 | // Opt represents a numerical option. 6 | type Opt struct { 7 | // ID is usually used to detect an option from a number in string. 8 | ID int 9 | // Description contains a human-readable description of an option. 10 | Description string 11 | } 12 | 13 | // StringOpt represents a string option. 14 | type StringOpt struct { 15 | // ID is usually used to detect an option from a substring in string. 16 | ID string 17 | // Description contains a human-readable description of an option. 18 | Description string 19 | } 20 | 21 | // UnknownOpt represents an option that was parsed incorrectly or was not parsed at all. 22 | var UnknownOpt = Opt{ID: -1, Description: "-"} 23 | 24 | // UnknownStringOpt represents a string option that was parsed incorrectly or was not parsed at all. 25 | var UnknownStringOpt = StringOpt{ID: "nil", Description: "Unknown"} 26 | 27 | // KillCmd is an artificial AT command that may be successfully sent to device in order 28 | // to emulate the response from it. In other words, if a connection with device stalled and 29 | // no bytes could be read, then this command is used to read something and then close the connection. 30 | const KillCmd = "AT_KILL" 31 | 32 | // NoopCmd is like a ping command that signals that the device is responsive. 33 | const NoopCmd = "AT" 34 | 35 | type ( 36 | optMap map[int]Opt 37 | stringOpts []StringOpt 38 | ) 39 | 40 | func (o optMap) Resolve(id int) Opt { 41 | if opt, ok := o[id]; ok { 42 | return opt 43 | } 44 | return UnknownOpt 45 | } 46 | 47 | func (s stringOpts) Resolve(str string) StringOpt { 48 | for _, v := range s { 49 | if strings.HasPrefix(str, v.ID) { 50 | return v 51 | } 52 | } 53 | return UnknownStringOpt 54 | } 55 | 56 | // DeviceState represents the device state including cellular options, 57 | // signal quality, current operator name, service status. 58 | type DeviceState struct { 59 | ServiceState Opt 60 | ServiceDomain Opt 61 | RoamingState Opt 62 | SystemMode Opt 63 | SystemSubmode Opt 64 | SimState Opt 65 | ModelName string 66 | OperatorName string 67 | IMEI string 68 | SignalStrength int 69 | } 70 | 71 | // NewDeviceState returns a clean state with unknown options. 72 | func NewDeviceState() *DeviceState { 73 | return &DeviceState{ 74 | ServiceState: UnknownOpt, 75 | ServiceDomain: UnknownOpt, 76 | RoamingState: UnknownOpt, 77 | SystemMode: UnknownOpt, 78 | SystemSubmode: UnknownOpt, 79 | SimState: UnknownOpt, 80 | } 81 | } 82 | 83 | var sim = optMap{ 84 | 0: Opt{0, "Invalid USIM card or pin code locked"}, 85 | 1: Opt{1, "Valid USIM card"}, 86 | 2: Opt{2, "USIM is invalid for cellular service"}, 87 | 3: Opt{3, "USIM is invalid for packet service"}, 88 | 4: Opt{4, "USIM is not valid for cellular nor packet services"}, 89 | 255: Opt{255, "USIM card is not exist"}, 90 | } 91 | 92 | // SimStates represent the possible data card states. 93 | var SimStates = struct { 94 | Resolve func(int) Opt 95 | 96 | Invalid Opt 97 | Valid Opt 98 | InvalidCS Opt 99 | InvalidPS Opt 100 | InvalidCSPS Opt 101 | NoCard Opt 102 | }{ 103 | func(id int) Opt { return sim.Resolve(id) }, 104 | 105 | sim[0], sim[1], sim[2], sim[3], sim[4], sim[255], 106 | } 107 | 108 | var service = optMap{ 109 | 0: Opt{0, "No service"}, 110 | 1: Opt{1, "Restricted service"}, 111 | 2: Opt{2, "Valid service"}, 112 | 3: Opt{3, "Restricted regional service"}, 113 | 4: Opt{4, "Power-saving and deep sleep state"}, 114 | } 115 | 116 | // ServiceStates represent the possible service states. 117 | var ServiceStates = struct { 118 | Resolve func(int) Opt 119 | 120 | None Opt 121 | Restricted Opt 122 | Valid Opt 123 | RestrictedRegional Opt 124 | PowerSaving Opt 125 | }{ 126 | func(id int) Opt { return service.Resolve(id) }, 127 | 128 | service[0], service[1], service[2], service[3], service[4], 129 | } 130 | 131 | var domain = optMap{ 132 | 0: Opt{0, "No service"}, 133 | 1: Opt{1, "Cellular service only"}, 134 | 2: Opt{2, "Packet service only"}, 135 | 3: Opt{3, "Packet and Cellular services"}, 136 | 4: Opt{4, "Searching"}, 137 | } 138 | 139 | // ServiceDomains represent the possible service domains. 140 | var ServiceDomains = struct { 141 | Resolve func(int) Opt 142 | 143 | None Opt 144 | Restricted Opt 145 | Valid Opt 146 | RestrictedRegional Opt 147 | PowerSaving Opt 148 | }{ 149 | func(id int) Opt { return domain.Resolve(id) }, 150 | 151 | domain[0], domain[1], 152 | domain[2], domain[3], domain[4], 153 | } 154 | 155 | var roaming = optMap{ 156 | 0: Opt{0, "Non roaming"}, 157 | 1: Opt{1, "Roaming"}, 158 | } 159 | 160 | // RoamingStates represent the state of roaming. 161 | var RoamingStates = struct { 162 | Resolve func(int) Opt 163 | 164 | NotRoaming Opt 165 | Roaming Opt 166 | }{ 167 | func(id int) Opt { return roaming.Resolve(id) }, 168 | 169 | roaming[0], roaming[1], 170 | } 171 | 172 | var mode = optMap{ 173 | 0: Opt{0, "No service"}, 174 | 1: Opt{1, "AMPS"}, 175 | 2: Opt{2, "CDMA"}, 176 | 3: Opt{3, "GSM/GPRS"}, 177 | 4: Opt{4, "HDR"}, 178 | 5: Opt{5, "WCDMA"}, 179 | 6: Opt{6, "GPS"}, 180 | 7: Opt{7, "GSM/WCDMA"}, 181 | 8: Opt{8, "CDMA/HDR HYBRID"}, 182 | 15: Opt{15, "TD-SCDMA"}, 183 | } 184 | 185 | // SystemModes represent the possible system operating modes. 186 | var SystemModes = struct { 187 | Resolve func(int) Opt 188 | 189 | NoService Opt 190 | AMPS Opt 191 | CDMA Opt 192 | GsmGprs Opt 193 | HDR Opt 194 | WCDMA Opt 195 | GPS Opt 196 | GsmWcdma Opt 197 | CdmaHdr Opt 198 | SCDMA Opt 199 | }{ 200 | func(id int) Opt { return mode.Resolve(id) }, 201 | 202 | mode[0], mode[1], mode[2], mode[3], mode[4], 203 | mode[5], mode[6], mode[7], mode[8], mode[15], 204 | } 205 | 206 | var submode = optMap{ 207 | 0: Opt{0, "No service"}, 208 | 1: Opt{1, "GSM"}, 209 | 2: Opt{2, "GPRS"}, 210 | 3: Opt{3, "EDGE"}, 211 | 4: Opt{4, "WCDMA"}, 212 | 5: Opt{5, "HSDPA"}, 213 | 6: Opt{6, "HSUPA"}, 214 | 7: Opt{7, "HSDPA and HSUPA"}, 215 | 8: Opt{8, "TD-SCDMA"}, 216 | 9: Opt{9, "HSPA+"}, 217 | 17: Opt{17, "HSPA+(64QAM)"}, 218 | 18: Opt{18, "HSPA+(MIMO)"}, 219 | } 220 | 221 | // SystemSubmodes represent the possible system operating submodes. 222 | var SystemSubmodes = struct { 223 | Resolve func(int) Opt 224 | 225 | NoService Opt 226 | GSM Opt 227 | GPRS Opt 228 | EDGE Opt 229 | WCDMA Opt 230 | HSDPA Opt 231 | HSUPA Opt 232 | HsdpaHsupa Opt 233 | SCDMA Opt 234 | HspaPlus Opt 235 | Hspa64QAM Opt 236 | HspaMIMO Opt 237 | }{ 238 | func(id int) Opt { return submode.Resolve(id) }, 239 | 240 | submode[0], submode[1], submode[2], submode[3], 241 | submode[4], submode[5], submode[6], submode[7], 242 | submode[8], submode[9], submode[17], submode[18], 243 | } 244 | 245 | var result = stringOpts{ 246 | {"AT", "Noop"}, 247 | {"OK", "Success"}, 248 | {"CONNECT", "Connect"}, 249 | {"RING", "Ringing"}, 250 | {"NO CARRIER", "No carrier"}, 251 | {"ERROR", "Error"}, 252 | {"NO DIALTONE", "No dialtone"}, 253 | {"BUSY", "Busy"}, 254 | {"NO ANSWER", "No answer"}, 255 | {"+CME ERROR:", "CME Error"}, 256 | {"+CMS ERROR:", "CMS Error"}, 257 | {"COMMAND NOT SUPPORT", "Command is not supported"}, 258 | {"TOO MANY PARAMETERS", "Too many parameters"}, 259 | {"AT_KILL", "Timeout"}, 260 | } 261 | 262 | // FinalResults represent the possible replies from a modem. 263 | var FinalResults = struct { 264 | Resolve func(string) StringOpt 265 | 266 | Noop StringOpt 267 | Ok StringOpt 268 | Connect StringOpt 269 | Ring StringOpt 270 | NoCarrier StringOpt 271 | Error StringOpt 272 | NoDialtone StringOpt 273 | Busy StringOpt 274 | NoAnswer StringOpt 275 | CmeError StringOpt 276 | CmsError StringOpt 277 | NotSupported StringOpt 278 | TooManyParameters StringOpt 279 | Timeout StringOpt 280 | }{ 281 | func(str string) StringOpt { return result.Resolve(str) }, 282 | 283 | result[0], result[1], result[2], result[3], 284 | result[4], result[5], result[6], result[7], 285 | result[8], result[9], result[10], result[11], 286 | result[12], result[13], 287 | } 288 | 289 | var resultReporting = optMap{ 290 | 0: Opt{0, "Disabled"}, 291 | 1: Opt{1, "Enabled"}, 292 | 2: Opt{2, "Exit"}, 293 | } 294 | 295 | // UssdResultReporting represents the available options of USSD reporting. 296 | var UssdResultReporting = struct { 297 | Resolve func(int) Opt 298 | 299 | Disable Opt 300 | Enable Opt 301 | Exit Opt 302 | }{ 303 | func(id int) Opt { return resultReporting.Resolve(id) }, 304 | 305 | resultReporting[0], 306 | resultReporting[1], 307 | resultReporting[2], 308 | } 309 | 310 | var reports = stringOpts{ 311 | {"+CUSD:", "USSD reply"}, 312 | {"+CMTI:", "Incoming SMS"}, 313 | {"^RSSI:", "Signal strength"}, 314 | {"^BOOT:", "Boot handshake"}, 315 | {"^MODE:", "System mode"}, 316 | {"^SRVST:", "Service state"}, 317 | {"^SIMST:", "Sim state"}, 318 | {"^STIN:", "STIN"}, 319 | {"+CLIP:", "Incoming Caller ID"}, 320 | } 321 | 322 | // Reports represent the possible state reports from a modem. 323 | var Reports = struct { 324 | Resolve func(string) StringOpt 325 | 326 | Ussd StringOpt 327 | Message StringOpt 328 | SignalStrength StringOpt 329 | BootHandshake StringOpt 330 | Mode StringOpt 331 | ServiceState StringOpt 332 | SimState StringOpt 333 | Stin StringOpt 334 | CallerID StringOpt 335 | }{ 336 | func(str string) StringOpt { return reports.Resolve(str) }, 337 | 338 | reports[0], reports[1], reports[2], reports[3], 339 | reports[4], reports[5], reports[6], reports[7], reports[8], 340 | } 341 | 342 | var mem = stringOpts{ 343 | {"ME", "NV RAM"}, 344 | {"MT", "ME-associated storage"}, 345 | {"SM", "Sim message storage"}, 346 | {"SR", "State report storage"}, 347 | } 348 | 349 | // MemoryTypes represent the available options of message storage. 350 | var MemoryTypes = struct { 351 | Resolve func(string) StringOpt 352 | 353 | NvRAM StringOpt 354 | Associated StringOpt 355 | Sim StringOpt 356 | StateReport StringOpt 357 | }{ 358 | func(str string) StringOpt { return mem.Resolve(str) }, 359 | 360 | mem[0], mem[1], mem[2], mem[3], 361 | } 362 | 363 | var delOpts = optMap{ 364 | 0: Opt{0, "Delete message by index"}, 365 | 1: Opt{1, "Delete all read messages except MO"}, 366 | 2: Opt{2, "Delete all read messages except unsent MO"}, 367 | 3: Opt{3, "Delete all except unread"}, 368 | 4: Opt{4, "Delete all messages"}, 369 | } 370 | 371 | // DeleteOptions represent the available options of message deletion masks. 372 | var DeleteOptions = struct { 373 | Resolve func(int) Opt 374 | 375 | Index Opt 376 | AllReadNotMO Opt 377 | AllReadNotUnsent Opt 378 | AllNotUnread Opt 379 | All Opt 380 | }{ 381 | func(id int) Opt { return resultReporting.Resolve(id) }, 382 | 383 | delOpts[0], delOpts[1], delOpts[2], delOpts[3], delOpts[4], 384 | } 385 | 386 | var msgFlags = optMap{ 387 | 0: Opt{0, "Unread"}, 388 | 1: Opt{1, "Read"}, 389 | 2: Opt{2, "Unsent"}, 390 | 3: Opt{3, "Sent"}, 391 | 4: Opt{4, "Any"}, 392 | } 393 | 394 | // MessageFlags represent the available states of messages in memory. 395 | var MessageFlags = struct { 396 | Resolve func(int) Opt 397 | 398 | Unread Opt 399 | Read Opt 400 | Unsent Opt 401 | Sent Opt 402 | Any Opt 403 | }{ 404 | func(id int) Opt { return resultReporting.Resolve(id) }, 405 | 406 | msgFlags[0], msgFlags[1], msgFlags[2], msgFlags[3], msgFlags[4], 407 | } 408 | 409 | var callerIDType = optMap{ 410 | 129: Opt{129, "Network Specific Caller ID"}, 411 | 145: Opt{145, "International Caller ID"}, 412 | } 413 | 414 | // CallerIDTypes represent the possible caller id types. 415 | var CallerIDTypes = struct { 416 | Resolve func(int) Opt 417 | 418 | NetworkSpecific Opt 419 | International Opt 420 | }{ 421 | func(id int) Opt { return callerIDType.Resolve(id) }, 422 | 423 | callerIDType[129], callerIDType[145], 424 | } 425 | 426 | var callerIDValidity = optMap{ 427 | 0: Opt{0, "Valid"}, 428 | 1: Opt{1, "Rejected by calling party"}, 429 | 2: Opt{2, "Denied by network"}, 430 | } 431 | 432 | // CallerIDValidityStates represent the possible caller id validity states. 433 | var CallerIDValidityStates = struct { 434 | Resolve func(int) Opt 435 | 436 | Valid Opt 437 | Rejected Opt 438 | Denied Opt 439 | }{ 440 | func(id int) Opt { return callerIDValidity.Resolve(id) }, 441 | 442 | callerIDValidity[0], callerIDValidity[1], callerIDValidity[2], 443 | } 444 | -------------------------------------------------------------------------------- /opts_test.go: -------------------------------------------------------------------------------- 1 | package at 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // Test the field access for an opt. 10 | func TestOpt(t *testing.T) { 11 | t.Parallel() 12 | 13 | opt := ServiceStates.Restricted 14 | assert.Equal(t, 1, opt.ID) 15 | assert.Equal(t, "Restricted service", opt.Description) 16 | } 17 | 18 | // Test the resolve function of an opt. 19 | func TestResolve(t *testing.T) { 20 | t.Parallel() 21 | 22 | opt := ServiceStates.Restricted 23 | assert.Equal(t, opt, ServiceStates.Resolve(1)) 24 | } 25 | 26 | // TODO: complete this suite in case of 100% coverage needed. 27 | -------------------------------------------------------------------------------- /pdu/7bit.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | const ( 10 | Esc byte = 0x1B // Esc code. 11 | Sub byte = 0x1A // Ctrl+Z code. 12 | CR byte = 0x0D // code. 13 | ) 14 | 15 | var ( 16 | crcr = []byte{CR, CR} 17 | cr = []byte{CR} 18 | ) 19 | 20 | const ( 21 | max byte = 0x7F 22 | unknown rune = '?' 23 | ) 24 | 25 | // ErrUnexpectedByte happens when someone tries to decode non GSM 7-bit encoded string. 26 | var ErrUnexpectedByte = errors.New("7bit decode: met an unexpected byte") 27 | 28 | // Is7BitEncodable reports whether s can be encoded using GSM 7-bit 29 | // encoding with default alphabet, without replacing or omitting characters. 30 | func Is7BitEncodable(s string) bool { 31 | for _, r := range s { 32 | if i := gsmTable.Index(r); i < 0 { 33 | if gsmEscapes.to7Bit(r) == byte(unknown) { 34 | return false 35 | } 36 | } 37 | } 38 | return true 39 | } 40 | 41 | // Encode7Bit encodes the given UTF-8 text into GSM 7-bit (3GPP TS 23.038) 42 | // encoding with packing. Invalid characters outside the 7-bit encoding 43 | // and shift table are replaced with "?". 44 | func Encode7Bit(str string) []byte { 45 | raw7 := make([]byte, 0, len(str)) 46 | for _, r := range str { 47 | if i := gsmTable.Index(r); i >= 0 { 48 | raw7 = append(raw7, byte(i)) 49 | } else { 50 | b := gsmEscapes.to7Bit(r) 51 | if b == byte(unknown) { 52 | raw7 = append(raw7, b) 53 | } else { 54 | raw7 = append(raw7, Esc, b) 55 | } 56 | } 57 | } 58 | return pack7Bit(raw7) 59 | } 60 | 61 | // Decode7Bit decodes the given GSM 7-bit packed octet data (3GPP TS 23.038) 62 | // into an UTF-8 encoded string. 63 | func Decode7Bit(octets []byte) (str string, err error) { 64 | raw7 := unpack7Bit(octets) 65 | var escaped bool 66 | var r rune 67 | for _, b := range raw7 { 68 | if b > max { 69 | err = ErrUnexpectedByte 70 | return 71 | } else if escaped { 72 | r = gsmEscapes.from7Bit(b) 73 | escaped = false 74 | } else if b == Esc { 75 | escaped = true 76 | continue 77 | } else { 78 | r = gsmTable.Rune(int(b)) 79 | } 80 | str += string(r) 81 | } 82 | return 83 | } 84 | 85 | func pad(n, block int) int { 86 | if n%block == 0 { 87 | return n 88 | } 89 | return (n/block + 1) * block 90 | } 91 | 92 | func blocks(n, block int) int { 93 | if n%block == 0 { 94 | return n / block 95 | } 96 | return n/block + 1 97 | } 98 | 99 | func pack7Bit(raw7 []byte) []byte { 100 | pack7 := make([]byte, blocks(len(raw7)*7, 8)) 101 | pack := func(out []byte, b byte, oct int, bit uint8) (int, uint8) { 102 | for i := uint8(0); i < 7; i++ { 103 | out[oct] |= b >> i & 1 << bit 104 | bit++ 105 | if bit == 8 { 106 | oct++ 107 | bit = 0 108 | } 109 | } 110 | return oct, bit 111 | } 112 | var oct int // current octet in pack7 113 | var bit uint8 // current bit in octet 114 | var b byte // current byte in raw7 115 | for i := range raw7 { 116 | b = raw7[i] 117 | oct, bit = pack(pack7, b, oct, bit) 118 | } 119 | // N.B. in order to not confuse 7 zero-bits with @ 120 | // code is added to the packed bits. 121 | if 8-bit == 7 { 122 | pack(pack7, CR, oct, bit) 123 | } else if bit == 0 && b == CR { 124 | // and if data ends with on the octet boundary, 125 | // then we add an additional octet with . See (3GPP TS 23.038). 126 | pack7 = append(pack7, 0x00) 127 | pack(pack7, CR, oct, bit) 128 | } 129 | return pack7 130 | } 131 | 132 | func unpack7Bit(pack7 []byte) []byte { 133 | raw7 := make([]byte, 0, len(pack7)) 134 | var sep byte // current septet 135 | var bit uint8 // current bit in septet 136 | for _, oct := range pack7 { 137 | for i := uint8(0); i < 8; i++ { 138 | sep |= oct >> i & 1 << bit 139 | bit++ 140 | if bit == 7 { 141 | raw7 = append(raw7, sep) 142 | sep = 0 143 | bit = 0 144 | } 145 | } 146 | } 147 | if bytes.HasSuffix(raw7, crcr) || bytes.HasSuffix(raw7, cr) { 148 | raw7 = raw7[:len(raw7)-1] 149 | } 150 | return raw7 151 | } 152 | 153 | func displayPack(buf []byte) (out string) { 154 | for i := 0; i < len(buf)*8; i++ { 155 | b := buf[i/8] 156 | if i%8 == 0 { 157 | out += fmt.Sprintf("\n%02X:", b) 158 | } 159 | off := 7 - uint8(i%8) 160 | out += fmt.Sprintf("%4d", b>>off&1) 161 | } 162 | return out 163 | } 164 | 165 | type escape struct { 166 | from byte 167 | to rune 168 | } 169 | 170 | type escapeTable [0x0A]escape 171 | 172 | func (et *escapeTable) to7Bit(r rune) byte { 173 | for _, esc := range et { 174 | if esc.to == r { 175 | return esc.from 176 | } 177 | } 178 | return byte(unknown) 179 | } 180 | 181 | func (et *escapeTable) from7Bit(b byte) rune { 182 | for _, esc := range et { 183 | if esc.from == b { 184 | return esc.to 185 | } 186 | } 187 | return unknown 188 | } 189 | 190 | var gsmEscapes = escapeTable{ 191 | {0x0A, 0x000C}, /* FORM FEED */ 192 | {0x14, 0x005E}, /* CIRCUMFLEX ACCENT */ 193 | {0x28, 0x007B}, /* LEFT CURLY BRACKET */ 194 | {0x29, 0x007D}, /* RIGHT CURLY BRACKET */ 195 | {0x2F, 0x005C}, /* REVERSE SOLIDUS */ 196 | {0x3C, 0x005B}, /* LEFT SQUARE BRACKET */ 197 | {0x3D, 0x007E}, /* TILDE */ 198 | {0x3E, 0x005D}, /* RIGHT SQUARE BRACKET */ 199 | {0x40, 0x007C}, /* VERTICAL LINE */ 200 | {0x65, 0x20AC}, /* EURO SIGN */ 201 | } 202 | 203 | type runeTable [0x80]rune 204 | 205 | func (rt *runeTable) Index(r rune) int { 206 | for i, c := range rt { 207 | if c == r { 208 | return i 209 | } 210 | } 211 | return -1 212 | } 213 | 214 | func (rt *runeTable) Rune(idx int) rune { 215 | if idx >= 0 && idx < len(rt) { 216 | return rt[idx] 217 | } 218 | return unknown 219 | } 220 | 221 | // Thanks, Jeroen @ Mobile Tidings. 222 | var gsmTable = runeTable{ 223 | /* 0x00 */ 0x0040, /* COMMERCIAL AT */ 224 | /* 0x01 */ 0x00A3, /* POUND SIGN */ 225 | /* 0x02 */ 0x0024, /* DOLLAR SIGN */ 226 | /* 0x03 */ 0x00A5, /* YEN SIGN */ 227 | /* 0x04 */ 0x00E8, /* LATIN SMALL LETTER E WITH GRAVE */ 228 | /* 0x05 */ 0x00E9, /* LATIN SMALL LETTER E WITH ACUTE */ 229 | /* 0x06 */ 0x00F9, /* LATIN SMALL LETTER U WITH GRAVE */ 230 | /* 0x07 */ 0x00EC, /* LATIN SMALL LETTER I WITH GRAVE */ 231 | /* 0x08 */ 0x00F2, /* LATIN SMALL LETTER O WITH GRAVE */ 232 | /* 0x09 */ 0x00E7, /* LATIN SMALL LETTER C WITH CEDILLA */ 233 | /* 0x0A */ 0x000A, /* LINE FEED */ 234 | /* 0x0B */ 0x00D8, /* LATIN CAPITAL LETTER O WITH STROKE */ 235 | /* 0x0C */ 0x00F8, /* LATIN SMALL LETTER O WITH STROKE */ 236 | /* 0x0D */ 0x000D, /* CARRIAGE RETURN */ 237 | /* 0x0E */ 0x00C5, /* LATIN CAPITAL LETTER A WITH RING ABOVE */ 238 | /* 0x0F */ 0x00E5, /* LATIN SMALL LETTER A WITH RING ABOVE */ 239 | /* 0x10 */ 0x0394, /* GREEK CAPITAL LETTER DELTA */ 240 | /* 0x11 */ 0x005F, /* LOW LINE */ 241 | /* 0x12 */ 0x03A6, /* GREEK CAPITAL LETTER PHI */ 242 | /* 0x13 */ 0x0393, /* GREEK CAPITAL LETTER GAMMA */ 243 | /* 0x14 */ 0x039B, /* GREEK CAPITAL LETTER LAMDA */ 244 | /* 0x15 */ 0x03A9, /* GREEK CAPITAL LETTER OMEGA */ 245 | /* 0x16 */ 0x03A0, /* GREEK CAPITAL LETTER PI */ 246 | /* 0x17 */ 0x03A8, /* GREEK CAPITAL LETTER PSI */ 247 | /* 0x18 */ 0x03A3, /* GREEK CAPITAL LETTER SIGMA */ 248 | /* 0x19 */ 0x0398, /* GREEK CAPITAL LETTER THETA */ 249 | /* 0x1A */ 0x039E, /* GREEK CAPITAL LETTER XI */ 250 | /* 0x1B */ 0x00A0, /* ESCAPE TO EXTENSION TABLE */ 251 | /* 0x1C */ 0x00C6, /* LATIN CAPITAL LETTER AE */ 252 | /* 0x1D */ 0x00E6, /* LATIN SMALL LETTER AE */ 253 | /* 0x1E */ 0x00DF, /* LATIN SMALL LETTER SHARP S (German) */ 254 | /* 0x1F */ 0x00C9, /* LATIN CAPITAL LETTER E WITH ACUTE */ 255 | /* 0x20 */ 0x0020, /* SPACE */ 256 | /* 0x21 */ 0x0021, /* EXCLAMATION MARK */ 257 | /* 0x22 */ 0x0022, /* QUOTATION MARK */ 258 | /* 0x23 */ 0x0023, /* NUMBER SIGN */ 259 | /* 0x24 */ 0x00A4, /* CURRENCY SIGN */ 260 | /* 0x25 */ 0x0025, /* PERCENT SIGN */ 261 | /* 0x26 */ 0x0026, /* AMPERSAND */ 262 | /* 0x27 */ 0x0027, /* APOSTROPHE */ 263 | /* 0x28 */ 0x0028, /* LEFT PARENTHESIS */ 264 | /* 0x29 */ 0x0029, /* RIGHT PARENTHESIS */ 265 | /* 0x2A */ 0x002A, /* ASTERISK */ 266 | /* 0x2B */ 0x002B, /* PLUS SIGN */ 267 | /* 0x2C */ 0x002C, /* COMMA */ 268 | /* 0x2D */ 0x002D, /* HYPHEN-MINUS */ 269 | /* 0x2E */ 0x002E, /* FULL STOP */ 270 | /* 0x2F */ 0x002F, /* SOLIDUS */ 271 | /* 0x30 */ 0x0030, /* DIGIT ZERO */ 272 | /* 0x31 */ 0x0031, /* DIGIT ONE */ 273 | /* 0x32 */ 0x0032, /* DIGIT TWO */ 274 | /* 0x33 */ 0x0033, /* DIGIT THREE */ 275 | /* 0x34 */ 0x0034, /* DIGIT FOUR */ 276 | /* 0x35 */ 0x0035, /* DIGIT FIVE */ 277 | /* 0x36 */ 0x0036, /* DIGIT SIX */ 278 | /* 0x37 */ 0x0037, /* DIGIT SEVEN */ 279 | /* 0x38 */ 0x0038, /* DIGIT EIGHT */ 280 | /* 0x39 */ 0x0039, /* DIGIT NINE */ 281 | /* 0x3A */ 0x003A, /* COLON */ 282 | /* 0x3B */ 0x003B, /* SEMICOLON */ 283 | /* 0x3C */ 0x003C, /* LESS-THAN SIGN */ 284 | /* 0x3D */ 0x003D, /* EQUALS SIGN */ 285 | /* 0x3E */ 0x003E, /* GREATER-THAN SIGN */ 286 | /* 0x3F */ 0x003F, /* QUESTION MARK */ 287 | /* 0x40 */ 0x00A1, /* INVERTED EXCLAMATION MARK */ 288 | /* 0x41 */ 0x0041, /* LATIN CAPITAL LETTER A */ 289 | /* 0x42 */ 0x0042, /* LATIN CAPITAL LETTER B */ 290 | /* 0x43 */ 0x0043, /* LATIN CAPITAL LETTER C */ 291 | /* 0x44 */ 0x0044, /* LATIN CAPITAL LETTER D */ 292 | /* 0x45 */ 0x0045, /* LATIN CAPITAL LETTER E */ 293 | /* 0x46 */ 0x0046, /* LATIN CAPITAL LETTER F */ 294 | /* 0x47 */ 0x0047, /* LATIN CAPITAL LETTER G */ 295 | /* 0x48 */ 0x0048, /* LATIN CAPITAL LETTER H */ 296 | /* 0x49 */ 0x0049, /* LATIN CAPITAL LETTER I */ 297 | /* 0x4A */ 0x004A, /* LATIN CAPITAL LETTER J */ 298 | /* 0x4B */ 0x004B, /* LATIN CAPITAL LETTER K */ 299 | /* 0x4C */ 0x004C, /* LATIN CAPITAL LETTER L */ 300 | /* 0x4D */ 0x004D, /* LATIN CAPITAL LETTER M */ 301 | /* 0x4E */ 0x004E, /* LATIN CAPITAL LETTER N */ 302 | /* 0x4F */ 0x004F, /* LATIN CAPITAL LETTER O */ 303 | /* 0x50 */ 0x0050, /* LATIN CAPITAL LETTER P */ 304 | /* 0x51 */ 0x0051, /* LATIN CAPITAL LETTER Q */ 305 | /* 0x52 */ 0x0052, /* LATIN CAPITAL LETTER R */ 306 | /* 0x53 */ 0x0053, /* LATIN CAPITAL LETTER S */ 307 | /* 0x54 */ 0x0054, /* LATIN CAPITAL LETTER T */ 308 | /* 0x55 */ 0x0055, /* LATIN CAPITAL LETTER U */ 309 | /* 0x56 */ 0x0056, /* LATIN CAPITAL LETTER V */ 310 | /* 0x57 */ 0x0057, /* LATIN CAPITAL LETTER W */ 311 | /* 0x58 */ 0x0058, /* LATIN CAPITAL LETTER X */ 312 | /* 0x59 */ 0x0059, /* LATIN CAPITAL LETTER Y */ 313 | /* 0x5A */ 0x005A, /* LATIN CAPITAL LETTER Z */ 314 | /* 0x5B */ 0x00C4, /* LATIN CAPITAL LETTER A WITH DIAERESIS */ 315 | /* 0x5C */ 0x00D6, /* LATIN CAPITAL LETTER O WITH DIAERESIS */ 316 | /* 0x5D */ 0x00D1, /* LATIN CAPITAL LETTER N WITH TILDE */ 317 | /* 0x5E */ 0x00DC, /* LATIN CAPITAL LETTER U WITH DIAERESIS */ 318 | /* 0x5F */ 0x00A7, /* SECTION SIGN */ 319 | /* 0x60 */ 0x00BF, /* INVERTED QUESTION MARK */ 320 | /* 0x61 */ 0x0061, /* LATIN SMALL LETTER A */ 321 | /* 0x62 */ 0x0062, /* LATIN SMALL LETTER B */ 322 | /* 0x63 */ 0x0063, /* LATIN SMALL LETTER C */ 323 | /* 0x64 */ 0x0064, /* LATIN SMALL LETTER D */ 324 | /* 0x65 */ 0x0065, /* LATIN SMALL LETTER E */ 325 | /* 0x66 */ 0x0066, /* LATIN SMALL LETTER F */ 326 | /* 0x67 */ 0x0067, /* LATIN SMALL LETTER G */ 327 | /* 0x68 */ 0x0068, /* LATIN SMALL LETTER H */ 328 | /* 0x69 */ 0x0069, /* LATIN SMALL LETTER I */ 329 | /* 0x6A */ 0x006A, /* LATIN SMALL LETTER J */ 330 | /* 0x6B */ 0x006B, /* LATIN SMALL LETTER K */ 331 | /* 0x6C */ 0x006C, /* LATIN SMALL LETTER L */ 332 | /* 0x6D */ 0x006D, /* LATIN SMALL LETTER M */ 333 | /* 0x6E */ 0x006E, /* LATIN SMALL LETTER N */ 334 | /* 0x6F */ 0x006F, /* LATIN SMALL LETTER O */ 335 | /* 0x70 */ 0x0070, /* LATIN SMALL LETTER P */ 336 | /* 0x71 */ 0x0071, /* LATIN SMALL LETTER Q */ 337 | /* 0x72 */ 0x0072, /* LATIN SMALL LETTER R */ 338 | /* 0x73 */ 0x0073, /* LATIN SMALL LETTER S */ 339 | /* 0x74 */ 0x0074, /* LATIN SMALL LETTER T */ 340 | /* 0x75 */ 0x0075, /* LATIN SMALL LETTER U */ 341 | /* 0x76 */ 0x0076, /* LATIN SMALL LETTER V */ 342 | /* 0x77 */ 0x0077, /* LATIN SMALL LETTER W */ 343 | /* 0x78 */ 0x0078, /* LATIN SMALL LETTER X */ 344 | /* 0x79 */ 0x0079, /* LATIN SMALL LETTER Y */ 345 | /* 0x7A */ 0x007A, /* LATIN SMALL LETTER Z */ 346 | /* 0x7B */ 0x00E4, /* LATIN SMALL LETTER A WITH DIAERESIS */ 347 | /* 0x7C */ 0x00F6, /* LATIN SMALL LETTER O WITH DIAERESIS */ 348 | /* 0x7D */ 0x00F1, /* LATIN SMALL LETTER N WITH TILDE */ 349 | /* 0x7E */ 0x00FC, /* LATIN SMALL LETTER U WITH DIAERESIS */ 350 | /* 0x7F */ 0x00E0, /* LATIN SMALL LETTER A WITH GRAVE */ 351 | } 352 | -------------------------------------------------------------------------------- /pdu/7bit_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/xlab/at/util" 9 | ) 10 | 11 | func TestIs7BitEncodable(t *testing.T) { 12 | t.Parallel() 13 | 14 | for _, r := range gsmTable { 15 | ok := Is7BitEncodable(string(r)) 16 | assert.True(t, ok, "'%c' should be 7bit-encodable, but wasn't", r) 17 | } 18 | for _, esc := range gsmEscapes { 19 | ok := Is7BitEncodable(string(esc.to)) 20 | assert.True(t, ok, "'%c' should be 7bit-encodable, but wasn't", esc.to) 21 | } 22 | } 23 | 24 | func TestEncode7Bit(t *testing.T) { 25 | t.Parallel() 26 | 27 | testcases := []struct { 28 | str string 29 | exp []byte 30 | }{ 31 | {"hello[world]! ы?", util.MustBytes("E8329BFDDEF0EE6F399BBCF18540BF1F")}, 32 | {"AAAAAAAAAAAAAAB\r", util.MustBytes("C16030180C0683C16030180C0A1B0D")}, 33 | {"AAAAAAAAAAAAAAB", util.MustBytes("C16030180C0683C16030180C0A1B")}, 34 | {"height of eifel", util.MustBytes("E872FA8CA683DE6650396D2EB31B")}, 35 | } 36 | for _, tc := range testcases { 37 | assert.Equal(t, tc.exp, Encode7Bit(tc.str)) 38 | } 39 | } 40 | 41 | func TestDecode7Bit(t *testing.T) { 42 | t.Parallel() 43 | 44 | testcases := []struct { 45 | exp string 46 | pack7 []byte 47 | }{ 48 | // ы -> ? 49 | {"hello[world]! ??", util.MustBytes("E8329BFDDEF0EE6F399BBCF18540BF1F")}, 50 | {"AAAAAAAAAAAAAAB\r", util.MustBytes("C16030180C0683C16030180C0A1B0D")}, 51 | {"AAAAAAAAAAAAAAB", util.MustBytes("C16030180C0683C16030180C0A1B")}, 52 | {"height of eifel", util.MustBytes("E872FA8CA683DE6650396D2EB31B")}, 53 | } 54 | for _, tc := range testcases { 55 | log.Println(displayPack(tc.pack7)) 56 | out, err := Decode7Bit(tc.pack7) 57 | assert.NoError(t, err) 58 | assert.Equal(t, tc.exp, out) 59 | } 60 | } 61 | 62 | func TestPack7Bit(t *testing.T) { 63 | t.Parallel() 64 | 65 | raw7 := []byte{Esc, 0x3c, Esc, 0x3e} 66 | exp := []byte{0x1b, 0xde, 0xc6, 0x7} 67 | assert.Equal(t, exp, pack7Bit(raw7)) 68 | } 69 | 70 | func TestUnpack7Bit(t *testing.T) { 71 | t.Parallel() 72 | 73 | pack7 := []byte{0x1b, 0xde, 0xc6, 0x7} 74 | exp := []byte{Esc, 0x3c, Esc, 0x3e} 75 | assert.Equal(t, exp, unpack7Bit(pack7)) 76 | } 77 | -------------------------------------------------------------------------------- /pdu/doc.go: -------------------------------------------------------------------------------- 1 | // Package pdu implements data encoding schemes described in 3GPP TS 23.040. 2 | // 3 | // Such schemes include: 4 | // - GSM 7-Bit text encoding, 5 | // - UCS2 (UTF-16) text encoding, and 6 | // - semi-octet encoding of integers. 7 | package pdu 8 | -------------------------------------------------------------------------------- /pdu/semi_octet.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import "fmt" 4 | 5 | // Swap semi-octets in octet. 6 | func Swap(octet byte) byte { 7 | return (octet << 4) | (octet >> 4 & 0x0F) 8 | } 9 | 10 | // Encode to semi-octets. 11 | func Encode(value int) byte { 12 | lo := byte(value % 10) 13 | hi := byte((value % 100) / 10) 14 | return hi<<4 | lo 15 | } 16 | 17 | // Decode form semi-octets. 18 | func Decode(octet byte) int { 19 | lo := octet & 0x0F 20 | hi := octet >> 4 & 0x0F 21 | return int(hi)*10 + int(lo) 22 | } 23 | 24 | // EncodeSemi packs the given numerical chunks in a semi-octet 25 | // representation as described in 3GPP TS 23.040. 26 | func EncodeSemi(chunks ...uint64) []byte { 27 | digits := make([]uint8, 0, len(chunks)) 28 | for _, c := range chunks { 29 | var bucket []uint8 30 | if c < 10 { 31 | digits = append(digits, 0) 32 | } 33 | for c > 0 { 34 | d := c % 10 35 | bucket = append(bucket, uint8(d)) 36 | c = (c - d) / 10 37 | } 38 | for i := range bucket { 39 | digits = append(digits, bucket[len(bucket)-1-i]) 40 | } 41 | } 42 | octets := make([]byte, 0, len(digits)/2+1) 43 | for i := 0; i < len(digits); i += 2 { 44 | if len(digits)-i < 2 { 45 | octets = append(octets, 0xF0|digits[i]) 46 | return octets 47 | } 48 | octets = append(octets, digits[i+1]<<4|digits[i]) 49 | } 50 | return octets 51 | } 52 | 53 | // DecodeSemi unpacks numerical chunks from the given semi-octet encoded data. 54 | func DecodeSemi(octets []byte) []int { 55 | chunks := make([]int, 0, len(octets)*2) 56 | for _, oct := range octets { 57 | half := oct >> 4 58 | if half == 0xF { 59 | chunks = append(chunks, int(oct&0x0F)) 60 | return chunks 61 | } 62 | chunks = append(chunks, int(oct&0x0F)*10+int(half)) 63 | } 64 | return chunks 65 | } 66 | 67 | // DecodeSemiAddress unpacks phone numbers from the given semi-octet encoded data. 68 | // This method is different from DecodeSemi because a 0x00 byte should be interpreted as 69 | // two distinct digits. There 0x00 will be "00". 70 | func DecodeSemiAddress(octets []byte) (str string) { 71 | for _, oct := range octets { 72 | half := oct >> 4 73 | if half == 0xF { 74 | str += fmt.Sprintf("%d", oct&0x0F) 75 | return 76 | } 77 | str += fmt.Sprintf("%d%d", oct&0x0F, half) 78 | } 79 | return 80 | } 81 | -------------------------------------------------------------------------------- /pdu/semi_octet_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestEncodeSemi(t *testing.T) { 10 | t.Parallel() 11 | 12 | out := EncodeSemi(14, 6, 26, 21, 36, 30, 16) 13 | exp := []byte{0x41, 0x60, 0x62, 0x12, 0x63, 0x03, 0x61} 14 | assert.Equal(t, exp, out) 15 | } 16 | 17 | func TestDecodeSemi(t *testing.T) { 18 | t.Parallel() 19 | 20 | oct := []byte{0x41, 0x60, 0x62, 0x12, 0x63, 0x03, 0x61} 21 | out := DecodeSemi(oct) 22 | exp := []int{14, 6, 26, 21, 36, 30, 16} 23 | assert.Equal(t, exp, out) 24 | } 25 | -------------------------------------------------------------------------------- /pdu/ucs2.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "errors" 5 | "unicode/utf16" 6 | ) 7 | 8 | // ErrUnevenNumber happens when the number of octets (bytes) in the input is uneven. 9 | var ErrUnevenNumber = errors.New("decode ucs2: uneven number of octets") 10 | 11 | // ErrIncorrectDataLength happens when the length of octets is less than the header, defined by the first entry of the octets. 12 | var ErrIncorrectDataLength = errors.New("decode ucs2: incorrect data length in first entry of octets") 13 | 14 | // EncodeUcs2 encodes the given UTF-8 text into UCS2 (UTF-16) encoding and returns the produced octets. 15 | func EncodeUcs2(str string) []byte { 16 | buf := utf16.Encode([]rune(str)) 17 | octets := make([]byte, 0, len(buf)*2) 18 | for _, n := range buf { 19 | octets = append(octets, byte(n&0xFF00>>8), byte(n&0x00FF)) 20 | } 21 | return octets 22 | } 23 | 24 | // DecodeUcs2 decodes the given UCS2 (UTF-16) octet data into a UTF-8 encoded string. 25 | func DecodeUcs2(octets []byte, startsWithHeader bool) (str string, err error) { 26 | octetsLng := len(octets) 27 | headerLng := 0 28 | 29 | if octetsLng == 0 { 30 | err = ErrIncorrectDataLength 31 | return 32 | } 33 | 34 | if startsWithHeader { 35 | // just ignore header 36 | headerLng = int(octets[0]) + 1 37 | if (octetsLng - headerLng) <= 0 { 38 | err = ErrIncorrectDataLength 39 | return 40 | } 41 | 42 | octetsLng = octetsLng - headerLng 43 | } 44 | 45 | if octetsLng%2 != 0 { 46 | err = ErrUnevenNumber 47 | return 48 | } 49 | buf := make([]uint16, 0, octetsLng/2) 50 | for i := 0; i < octetsLng; i += 2 { 51 | buf = append(buf, uint16(octets[i+headerLng])<<8|uint16(octets[i+1+headerLng])) 52 | } 53 | runes := utf16.Decode(buf) 54 | return string(runes), nil 55 | } 56 | -------------------------------------------------------------------------------- /pdu/usc2_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var testStringUcs2 = "Этот абонент звонил вам 2 раза" 11 | 12 | var testOctetsUcs2 = []byte{ 13 | 0x04, 0x2D, 0x04, 0x42, 0x04, 0x3E, 0x04, 0x42, 14 | 0x00, 0x20, 0x04, 0x30, 0x04, 0x31, 0x04, 0x3E, 15 | 0x04, 0x3D, 0x04, 0x35, 0x04, 0x3D, 0x04, 0x42, 16 | 0x00, 0x20, 0x04, 0x37, 0x04, 0x32, 0x04, 0x3E, 17 | 0x04, 0x3D, 0x04, 0x38, 0x04, 0x3B, 0x00, 0x20, 18 | 0x04, 0x32, 0x04, 0x30, 0x04, 0x3C, 0x00, 0x20, 19 | 0x00, 0x32, 0x00, 0x20, 0x04, 0x40, 0x04, 0x30, 20 | 0x04, 0x37, 0x04, 0x30, 21 | } 22 | 23 | func TestEncodeUcs2(t *testing.T) { 24 | t.Parallel() 25 | 26 | out := EncodeUcs2(testStringUcs2) 27 | exp := testOctetsUcs2 28 | assert.Equal(t, exp, out) 29 | } 30 | 31 | func TestDecodeUcs2(t *testing.T) { 32 | t.Parallel() 33 | 34 | oct := testOctetsUcs2 35 | out, err := DecodeUcs2(oct, false) 36 | exp := testStringUcs2 37 | require.NoError(t, err) 38 | assert.Equal(t, exp, out) 39 | } 40 | -------------------------------------------------------------------------------- /sms/encoding.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | // Encoding represents the encoding of message's text data. 4 | type Encoding byte 5 | 6 | // Encodings represent the possible encodings of message's text data. 7 | var Encodings = struct { 8 | Gsm7Bit Encoding 9 | UCS2 Encoding 10 | Gsm7Bit_2 Encoding 11 | Gsm7Bit_3 Encoding 12 | }{ 13 | 0x00, 0x08, 0x11, 0x01, 14 | } 15 | -------------------------------------------------------------------------------- /sms/message_type.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | // MessageType represents the message's type. 4 | type MessageType byte 5 | 6 | // MessageTypes represent the possible message's types (3GPP TS 23.040). 7 | var MessageTypes = struct { 8 | Deliver MessageType 9 | DeliverReport MessageType 10 | StatusReport MessageType 11 | Command MessageType 12 | Submit MessageType 13 | SubmitReport MessageType 14 | }{ 15 | 0x00, 0x00, 16 | 0x02, 0x02, 17 | 0x01, 0x01, 18 | } 19 | -------------------------------------------------------------------------------- /sms/phone_number.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/xlab/at/pdu" 10 | ) 11 | 12 | // PhoneNumber represents the address in either local or international format. 13 | type PhoneNumber string 14 | 15 | // PhoneNumberType represents Type-of-Number, as specified in 3GPP 16 | // TS 23.040 version 16.0.0 release 16, section 9.1.2.5. 17 | type PhoneNumberType byte 18 | 19 | // PhoneNumberTypes are all known PhoneNumberType values. 20 | var PhoneNumberTypes = struct { 21 | Unknown PhoneNumberType 22 | International PhoneNumberType 23 | National PhoneNumberType 24 | NetworkSpecific PhoneNumberType 25 | Subscriber PhoneNumberType 26 | Alphanumeric PhoneNumberType 27 | Abbreviated PhoneNumberType 28 | Reserved PhoneNumberType // for future extension 29 | }{ 30 | Unknown: 0 << 4, 31 | International: 1 << 4, 32 | National: 2 << 4, 33 | NetworkSpecific: 3 << 4, 34 | Subscriber: 4 << 4, 35 | Alphanumeric: 5 << 4, 36 | Abbreviated: 6 << 4, 37 | Reserved: 7 << 4, 38 | } 39 | 40 | // NumberingPlan represents Numbering-plan-identification, as specified 41 | // in 3GPP TS 23.040 version 16.0.0 release 16, section 9.1.2.5. 42 | type NumberingPlan byte 43 | 44 | // NumberingPlans are all known NumberingPlan valus. Other values are 45 | // reserved. 46 | var NumberingPlans = struct { 47 | Unknown NumberingPlan 48 | E164 NumberingPlan // ISDN/telephone numbering plan 49 | X121 NumberingPlan // Data numbering plan 50 | Telex NumberingPlan 51 | ServiceCentreSpecificA NumberingPlan // used to indicate a numbering plan specific to ESME attached to the SMSC 52 | ServiceCentreSpecificB NumberingPlan // used to indicate a numbering plan specific to ESME attached to the SMSC 53 | National NumberingPlan 54 | Private NumberingPlan 55 | ERMES NumberingPlan 56 | Reserved NumberingPlan // for future extension 57 | }{ 58 | Unknown: 0b0000, 59 | E164: 0b0001, 60 | X121: 0b0011, 61 | Telex: 0b0100, 62 | ServiceCentreSpecificA: 0b0101, 63 | ServiceCentreSpecificB: 0b0110, 64 | National: 0b1000, 65 | Private: 0b1001, 66 | ERMES: 0b1010, 67 | Reserved: 0b1111, 68 | } 69 | 70 | // PDU returns the number of digits in address and octets of semi-octet encoded address. 71 | func (p PhoneNumber) PDU() (int, []byte, error) { 72 | digitStr := strings.TrimPrefix(string(p), "+") 73 | var str string 74 | for _, r := range digitStr { 75 | if r >= '0' && r <= '9' { 76 | str = str + string(r) 77 | } 78 | } 79 | n := len(str) 80 | number, err := strconv.ParseUint(str, 10, 64) 81 | if err != nil { 82 | return 0, nil, err 83 | } 84 | var buf bytes.Buffer 85 | buf.WriteByte(p.Type()) 86 | buf.Write(pdu.EncodeSemi(number)) 87 | return n, buf.Bytes(), nil 88 | } 89 | 90 | // Type returns the type of address (a combination of type-of-number and 91 | // numbering-plan-identification). Currently, only national and 92 | // international E.164 numbers are understood. While ReadFrom() can 93 | // parse alphanumeric numbers, Type() doesn't recognize it. 94 | func (p PhoneNumber) Type() byte { 95 | typ := PhoneNumberTypes.National 96 | if strings.HasPrefix(string(p), "+") { 97 | typ = PhoneNumberTypes.International 98 | } 99 | return 0x80 | byte(typ) | byte(NumberingPlans.E164) 100 | } 101 | 102 | // ReadFrom constructs an address from the semi-decoded version in the supplied byte slice. 103 | func (p *PhoneNumber) ReadFrom(octets []byte) error { 104 | if len(octets) < 1 { 105 | return ErrIncorrectSize 106 | } 107 | 108 | typ := PhoneNumberType(octets[0] & 0b0111_0000) 109 | switch typ { 110 | case PhoneNumberTypes.Alphanumeric: 111 | addr, err := pdu.Decode7Bit(octets[1:]) 112 | if err != nil { 113 | return err 114 | } 115 | *p = PhoneNumber(addr) 116 | case PhoneNumberTypes.International: 117 | addr := pdu.DecodeSemiAddress(octets[1:]) 118 | *p = PhoneNumber("+" + addr) 119 | case PhoneNumberTypes.National: 120 | addr := pdu.DecodeSemiAddress(octets[1:]) 121 | *p = PhoneNumber(addr) 122 | default: 123 | return fmt.Errorf("%w: Type(0x%x)", ErrUnsupportedTypeOfNumber, typ) 124 | } 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /sms/phone_number_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "github.com/xlab/at/util" 9 | ) 10 | 11 | func TestPhoneNumber(t *testing.T) { 12 | t.Parallel() 13 | 14 | type testcase struct { 15 | pdu []byte 16 | number string 17 | typ PhoneNumberType 18 | } 19 | 20 | for name, tc := range map[string]testcase{ 21 | "international": { 22 | pdu: util.MustBytes("9121436587F9"), 23 | number: "+123456789", 24 | typ: PhoneNumberTypes.International, 25 | }, 26 | "national": { 27 | pdu: util.MustBytes("A11032547698"), 28 | number: "0123456789", 29 | typ: PhoneNumberTypes.National, 30 | }, 31 | "alphanumeric": { 32 | pdu: util.MustBytes("D061F1985C3603"), 33 | number: "abcdef", 34 | // FIXME: we don't have proper support for alphanumeric numbers 35 | // yet, so Type() will just use "national" as type. 36 | typ: PhoneNumberTypes.National, 37 | }, 38 | } { 39 | tc := tc 40 | t.Run(name, func(t *testing.T) { 41 | t.Parallel() 42 | 43 | var subject PhoneNumber 44 | err := subject.ReadFrom(tc.pdu) 45 | require.NoError(t, err) 46 | 47 | assert.EqualValues(t, tc.number, subject) 48 | assert.Equal(t, 0x81|byte(tc.typ), subject.Type()) 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /sms/sms.go: -------------------------------------------------------------------------------- 1 | // Package sms allows to encode and decode SMS messages into/from PDU format as described in 3GPP TS 23.040. 2 | package sms 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "io" 8 | "unicode/utf8" 9 | 10 | "github.com/xlab/at/pdu" 11 | ) 12 | 13 | // Common errors. 14 | var ( 15 | ErrUnknownEncoding = errors.New("sms: unsupported encoding") 16 | ErrUnknownMessageType = errors.New("sms: unsupported message type") 17 | ErrIncorrectSize = errors.New("sms: decoded incorrect size of field") 18 | ErrNonRelative = errors.New("sms: non-relative validity period support is not implemented yet") 19 | ErrIncorrectUserDataHeaderLength = errors.New("sms: incorrect user data header length ") 20 | ErrUnsupportedTypeOfNumber = errors.New("sms: unsupported type-of-number") 21 | ) 22 | 23 | // Message represents an SMS message, including some advanced fields. This 24 | // is a user-friendly high-level representation that should be used around. 25 | // Complies with 3GPP TS 23.040. 26 | type Message struct { 27 | Type MessageType 28 | Encoding Encoding 29 | VP ValidityPeriod 30 | VPFormat ValidityPeriodFormat 31 | ServiceCenterTime Timestamp 32 | DischargeTime Timestamp 33 | ServiceCenterAddress PhoneNumber 34 | Address PhoneNumber 35 | Text string 36 | UserDataHeader UserDataHeader 37 | 38 | // Advanced 39 | MessageReference byte 40 | Status Status 41 | ReplyPathExists bool 42 | UserDataStartsWithHeader bool 43 | StatusReportIndication bool 44 | StatusReportRequest bool 45 | StatusReportQualificator bool 46 | MoreMessagesToSend bool 47 | LoopPrevention bool 48 | RejectDuplicates bool 49 | } 50 | 51 | func blocks(n, block int) int { 52 | if n%block == 0 { 53 | return n / block 54 | } 55 | return n/block + 1 56 | } 57 | 58 | func cutStr(str string, n int) string { 59 | runes := []rune(str) 60 | if n < len(str) { 61 | return string(runes[0:n]) 62 | } 63 | return str 64 | } 65 | 66 | // PDU serializes the message into octets ready to be transferred. 67 | // Returns the number of TPDU bytes in the produced PDU. 68 | // Complies with 3GPP TS 23.040. 69 | func (s *Message) PDU() (int, []byte, error) { 70 | var buf bytes.Buffer 71 | if len(s.ServiceCenterAddress) < 1 { 72 | buf.WriteByte(0x00) // SMSC info length 73 | } else { 74 | _, octets, err := s.ServiceCenterAddress.PDU() 75 | if err != nil { 76 | return 0, nil, err 77 | } 78 | buf.WriteByte(byte(len(octets))) 79 | buf.Write(octets) 80 | } 81 | 82 | var n int 83 | var err error 84 | 85 | switch s.Type { 86 | case MessageTypes.Deliver: 87 | n, err = s.encodeDeliver(&buf) 88 | case MessageTypes.Submit: 89 | n, err = s.encodeSubmit(&buf) 90 | case MessageTypes.StatusReport: 91 | n, err = s.encodeStatusReport(&buf) 92 | default: 93 | err = ErrUnknownMessageType 94 | } 95 | 96 | if err != nil { 97 | return 0, nil, err 98 | } 99 | return n, buf.Bytes(), nil 100 | } 101 | 102 | func (s *Message) encodeDeliver(buf *bytes.Buffer) (n int, err error) { 103 | var sms smsDeliver 104 | sms.MessageTypeIndicator = byte(s.Type) 105 | sms.MoreMessagesToSend = s.MoreMessagesToSend 106 | sms.LoopPrevention = s.LoopPrevention 107 | sms.ReplyPath = s.ReplyPathExists 108 | sms.UserDataHeaderIndicator = s.UserDataStartsWithHeader 109 | sms.StatusReportIndication = s.StatusReportIndication 110 | 111 | addrLen, addr, err := s.Address.PDU() 112 | if err != nil { 113 | return 0, err 114 | } 115 | var addrBuf bytes.Buffer 116 | addrBuf.WriteByte(byte(addrLen)) 117 | addrBuf.Write(addr) 118 | sms.OriginatingAddress = addrBuf.Bytes() 119 | 120 | sms.ProtocolIdentifier = 0x00 // Short Message Type 0 121 | sms.DataCodingScheme = byte(s.Encoding) 122 | sms.ServiceCentreTimestamp = s.ServiceCenterTime.PDU() 123 | sms.UserData, sms.UserDataLength, err = s.encodedUserData() 124 | if err != nil { 125 | return 0, err 126 | } 127 | 128 | return buf.Write(sms.Bytes()) 129 | } 130 | 131 | func (s *Message) encodeSubmit(buf *bytes.Buffer) (n int, err error) { 132 | var sms smsSubmit 133 | sms.MessageTypeIndicator = byte(s.Type) 134 | sms.RejectDuplicates = s.RejectDuplicates 135 | sms.ValidityPeriodFormat = byte(s.VPFormat) 136 | sms.ReplyPath = s.ReplyPathExists 137 | sms.UserDataHeaderIndicator = s.UserDataStartsWithHeader 138 | sms.StatusReportRequest = s.StatusReportRequest 139 | sms.MessageReference = s.MessageReference 140 | 141 | addrLen, addr, err := s.Address.PDU() 142 | if err != nil { 143 | return 0, err 144 | } 145 | var addrBuf bytes.Buffer 146 | addrBuf.WriteByte(byte(addrLen)) 147 | addrBuf.Write(addr) 148 | sms.DestinationAddress = addrBuf.Bytes() 149 | 150 | sms.ProtocolIdentifier = 0x00 // Short Message Type 0 151 | sms.DataCodingScheme = byte(s.Encoding) 152 | 153 | switch s.VPFormat { 154 | case ValidityPeriodFormats.Relative: 155 | sms.ValidityPeriod = byte(s.VP.Octet()) 156 | case ValidityPeriodFormats.Absolute, ValidityPeriodFormats.Enhanced: 157 | return 0, ErrNonRelative 158 | } 159 | 160 | sms.UserData, sms.UserDataLength, err = s.encodedUserData() 161 | if err != nil { 162 | return 0, err 163 | } 164 | return buf.Write(sms.Bytes()) 165 | } 166 | 167 | func (s *Message) encodeStatusReport(buf *bytes.Buffer) (n int, err error) { 168 | var sms smsStatusReport 169 | sms.MessageTypeIndicator = byte(s.Type) 170 | sms.UserDataHeaderIndicator = s.UserDataStartsWithHeader 171 | sms.MoreMessagesToSend = s.MoreMessagesToSend 172 | sms.LoopPrevention = s.LoopPrevention 173 | sms.StatusReportQualificator = s.StatusReportQualificator 174 | sms.MessageReference = s.MessageReference 175 | 176 | addrLen, addr, err := s.Address.PDU() 177 | if err != nil { 178 | return 0, err 179 | } 180 | var addrBuf bytes.Buffer 181 | addrBuf.WriteByte(byte(addrLen)) 182 | addrBuf.Write(addr) 183 | sms.DestinationAddress = addrBuf.Bytes() 184 | 185 | sms.ServiceCentreTimestamp = s.ServiceCenterTime.PDU() 186 | sms.DischargeTimestamp = s.DischargeTime.PDU() 187 | sms.Status = byte(s.Status) 188 | sms.UserData, sms.UserDataLength, err = s.encodedUserData() 189 | if err != nil { 190 | return 0, err 191 | } 192 | 193 | return buf.Write(sms.Bytes()) 194 | } 195 | 196 | // ReadFrom constructs a message from the supplied PDU octets. Returns the number of bytes read. 197 | // Complies with 3GPP TS 23.040. 198 | func (s *Message) ReadFrom(octets []byte) (n int, err error) { 199 | *s = Message{} 200 | buf := bytes.NewReader(octets) 201 | scLen, err := buf.ReadByte() 202 | n++ 203 | if err != nil { 204 | return 205 | } 206 | if scLen > 16 { 207 | return 0, ErrIncorrectSize 208 | } 209 | addr := make([]byte, scLen) 210 | off, err := io.ReadFull(buf, addr) 211 | n += off 212 | if err != nil { 213 | return 214 | } 215 | s.ServiceCenterAddress.ReadFrom(addr) 216 | msgType, err := buf.ReadByte() 217 | n++ 218 | if err != nil { 219 | return 220 | } 221 | n-- 222 | buf.UnreadByte() 223 | s.Type = MessageType(msgType & 0x03) 224 | 225 | var decBytes int 226 | octets = octets[1+scLen:] 227 | 228 | switch s.Type { 229 | case MessageTypes.Deliver: 230 | decBytes, err = s.decodeDeliver(octets) 231 | case MessageTypes.Submit: 232 | decBytes, err = s.decodeSubmit(octets) 233 | case MessageTypes.StatusReport: 234 | decBytes, err = s.decodeStatusReport(octets) 235 | default: 236 | return n, ErrUnknownMessageType 237 | } 238 | 239 | n += decBytes 240 | return n, err 241 | } 242 | 243 | func (s *Message) decodeDeliver(data []byte) (n int, err error) { 244 | var sms smsDeliver 245 | n, err = sms.FromBytes(data) 246 | if err != nil { 247 | return 248 | } 249 | s.MoreMessagesToSend = sms.MoreMessagesToSend 250 | s.LoopPrevention = sms.LoopPrevention 251 | s.ReplyPathExists = sms.ReplyPath 252 | s.UserDataStartsWithHeader = sms.UserDataHeaderIndicator 253 | if sms.UserDataHeaderIndicator { 254 | err = s.UserDataHeader.ReadFrom(sms.UserData) 255 | if err != nil { 256 | return 257 | } 258 | } 259 | s.StatusReportIndication = sms.StatusReportIndication 260 | s.Address.ReadFrom(sms.OriginatingAddress[1:]) 261 | s.Encoding = Encoding(sms.DataCodingScheme) 262 | s.ServiceCenterTime.ReadFrom(sms.ServiceCentreTimestamp) 263 | err = s.decodeUserData(sms.UserData, sms.UserDataLength) 264 | return n, err 265 | } 266 | 267 | func (s *Message) decodeSubmit(data []byte) (n int, err error) { 268 | var sms smsSubmit 269 | n, err = sms.FromBytes(data) 270 | if err != nil { 271 | return 272 | } 273 | s.RejectDuplicates = sms.RejectDuplicates 274 | 275 | switch s.VPFormat { 276 | case ValidityPeriodFormats.Absolute, ValidityPeriodFormats.Enhanced: 277 | return n, ErrNonRelative 278 | default: 279 | s.VPFormat = ValidityPeriodFormat(sms.ValidityPeriodFormat) 280 | } 281 | 282 | s.MessageReference = sms.MessageReference 283 | s.ReplyPathExists = sms.ReplyPath 284 | s.UserDataStartsWithHeader = sms.UserDataHeaderIndicator 285 | s.StatusReportRequest = sms.StatusReportRequest 286 | s.Address.ReadFrom(sms.DestinationAddress[1:]) 287 | s.Encoding = Encoding(sms.DataCodingScheme) 288 | 289 | if s.VPFormat != ValidityPeriodFormats.FieldNotPresent { 290 | s.VP.ReadFrom(sms.ValidityPeriod) 291 | } 292 | err = s.decodeUserData(sms.UserData, sms.UserDataLength) 293 | return n, err 294 | } 295 | 296 | func (s *Message) decodeStatusReport(data []byte) (n int, err error) { 297 | var sms smsStatusReport 298 | n, err = sms.FromBytes(data) 299 | if err != nil { 300 | return 301 | } 302 | s.MessageReference = sms.MessageReference 303 | s.MoreMessagesToSend = sms.MoreMessagesToSend 304 | s.LoopPrevention = sms.LoopPrevention 305 | s.UserDataStartsWithHeader = sms.UserDataHeaderIndicator 306 | if sms.UserDataHeaderIndicator { 307 | err = s.UserDataHeader.ReadFrom(sms.UserData) 308 | if err != nil { 309 | return 310 | } 311 | } 312 | s.StatusReportQualificator = sms.StatusReportQualificator 313 | s.Status = Status(sms.Status) 314 | s.Address.ReadFrom(sms.DestinationAddress[1:]) 315 | s.Encoding = Encoding(sms.DataCodingScheme) 316 | s.ServiceCenterTime.ReadFrom(sms.ServiceCentreTimestamp) 317 | s.DischargeTime.ReadFrom(sms.DischargeTimestamp) 318 | err = s.decodeUserData(sms.UserData, sms.UserDataLength) 319 | return n, err 320 | } 321 | 322 | func (s *Message) encodedUserData() (userData []byte, length byte, err error) { 323 | switch s.Encoding { 324 | case Encodings.Gsm7Bit, Encodings.Gsm7Bit_2, Encodings.Gsm7Bit_3: 325 | userData = pdu.Encode7Bit(s.Text) 326 | length = byte(utf8.RuneCountInString(s.Text)) 327 | case Encodings.UCS2: 328 | userData = pdu.EncodeUcs2(s.Text) 329 | length = byte(len(userData)) 330 | default: 331 | err = ErrUnknownEncoding 332 | } 333 | 334 | return 335 | } 336 | 337 | func (s *Message) decodeUserData(data []byte, dataLen byte) (err error) { 338 | switch s.Encoding { 339 | case Encodings.Gsm7Bit, Encodings.Gsm7Bit_2, Encodings.Gsm7Bit_3: 340 | if s.Text, err = pdu.Decode7Bit(data); err != nil { 341 | return 342 | } 343 | s.Text = cutStr(s.Text, int(dataLen)) 344 | case Encodings.UCS2: 345 | s.Text, err = pdu.DecodeUcs2(data, s.UserDataStartsWithHeader) 346 | default: 347 | return ErrUnknownEncoding 348 | } 349 | return err 350 | } 351 | -------------------------------------------------------------------------------- /sms/sms_deliver.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // Low-level representation of an deliver-type SMS message (3GPP TS 23.040). 9 | type smsDeliver struct { 10 | MessageTypeIndicator byte 11 | MoreMessagesToSend bool 12 | LoopPrevention bool 13 | ReplyPath bool 14 | UserDataHeaderIndicator bool 15 | StatusReportIndication bool 16 | 17 | OriginatingAddress []byte 18 | ProtocolIdentifier byte 19 | DataCodingScheme byte 20 | ServiceCentreTimestamp []byte 21 | UserDataLength byte 22 | UserData []byte 23 | } 24 | 25 | func (s *smsDeliver) Bytes() []byte { 26 | var buf bytes.Buffer 27 | header := s.MessageTypeIndicator // 0-1 bits 28 | if !s.MoreMessagesToSend { 29 | header |= 0x01 << 2 // 2 bit 30 | } 31 | if s.LoopPrevention { 32 | header |= 0x01 << 3 // 3 bit 33 | } 34 | if s.StatusReportIndication { 35 | header |= 0x01 << 4 // 4 bit 36 | } 37 | if s.UserDataHeaderIndicator { 38 | header |= 0x01 << 5 // 5 bit 39 | } 40 | if s.ReplyPath { 41 | header |= 0x01 << 6 // 6 bit 42 | } 43 | buf.WriteByte(header) 44 | buf.Write(s.OriginatingAddress) 45 | buf.WriteByte(s.ProtocolIdentifier) 46 | buf.WriteByte(s.DataCodingScheme) 47 | buf.Write(s.ServiceCentreTimestamp) 48 | buf.WriteByte(s.UserDataLength) 49 | buf.Write(s.UserData) 50 | return buf.Bytes() 51 | } 52 | 53 | // GSM 03.** 54 | // The TP-User-Data-Header-Indicator is a 1 bit field within bit 6 of the first octet of an 55 | // SMS-SUBMIT and SMS-DELIVER PDU and has the following values. 56 | // Bit no. 6 0 The TP-UD field contains only the short message 1 The beginning of the TP-UD 57 | // field contains a Header in addition to the short message 58 | 59 | // The TP-Reply-Path is a 1-bit field, located within bit no 7 of the first octet of both 60 | // SMS-DELIVER and SMS-SUBMIT, and to be given the following values: 61 | // Bit no 7: 0 TP-Reply-Path parameter is not set in this SMS-SUBMIT/DELIVER 62 | // 1 TP-Reply-Path parameter is set in this SMS-SUBMIT/DELIVER 63 | 64 | // TP-OA TP-Originating-Address 2-12 octets 65 | // Each address field of the SM-TL consists of the following sub-fields: An Address-Length 66 | // field of one octet, a Type-of-Address field of one octet, and one Address-Value field 67 | // of variable length 68 | // 69 | // The Address-Length field is an integer representation of the number of useful semi-octets 70 | // within the Address-Value field, i.e. excludes any semi octet containing only fill bits. 71 | 72 | func (s *smsDeliver) FromBytes(octets []byte) (n int, err error) { //nolint:funlen 73 | buf := bytes.NewReader(octets) 74 | *s = smsDeliver{} 75 | header, err := buf.ReadByte() 76 | n++ 77 | if err != nil { 78 | return 79 | } 80 | s.MessageTypeIndicator = header & 0x03 81 | if header>>2&0x01 == 0x00 { 82 | s.MoreMessagesToSend = true 83 | } 84 | if header>>3&0x01 == 0x01 { 85 | s.LoopPrevention = true 86 | } 87 | if header>>4&0x01 == 0x01 { 88 | s.StatusReportIndication = true 89 | } 90 | 91 | s.UserDataHeaderIndicator = header&(0x01<<6) != 0 92 | s.ReplyPath = header&(0x01<<7) != 0 93 | 94 | oaLen, err := buf.ReadByte() 95 | n++ 96 | if err != nil { 97 | return 98 | } 99 | buf.UnreadByte() // will read length again 100 | n-- 101 | s.OriginatingAddress = make([]byte, blocks(int(oaLen), 2)+2) 102 | off, err := io.ReadFull(buf, s.OriginatingAddress) 103 | n += off 104 | if err != nil { 105 | return 106 | } 107 | s.ProtocolIdentifier, err = buf.ReadByte() 108 | n++ 109 | if err != nil { 110 | return 111 | } 112 | s.DataCodingScheme, err = buf.ReadByte() 113 | n++ 114 | if err != nil { 115 | return 116 | } 117 | s.ServiceCentreTimestamp = make([]byte, 7) 118 | off, err = io.ReadFull(buf, s.ServiceCentreTimestamp) 119 | n += off 120 | if err != nil { 121 | return 122 | } 123 | s.UserDataLength, err = buf.ReadByte() 124 | n++ 125 | if err != nil { 126 | return 127 | } 128 | s.UserData = make([]byte, int(s.UserDataLength)) 129 | off, _ = io.ReadFull(buf, s.UserData) 130 | s.UserData = s.UserData[:off] 131 | n += off 132 | return n, nil 133 | } 134 | -------------------------------------------------------------------------------- /sms/sms_status_report.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // Low-level representation of an status-report-type SMS message (3GPP TS 23.040). 9 | type smsStatusReport struct { 10 | MessageTypeIndicator byte 11 | MoreMessagesToSend bool 12 | LoopPrevention bool 13 | UserDataHeaderIndicator bool 14 | StatusReportQualificator bool 15 | 16 | MessageReference byte 17 | DestinationAddress []byte 18 | Parameters byte 19 | ProtocolIdentifier byte 20 | DataCodingScheme byte 21 | ServiceCentreTimestamp []byte 22 | DischargeTimestamp []byte 23 | Status byte 24 | UserDataLength byte 25 | UserData []byte 26 | } 27 | 28 | func (s *smsStatusReport) Bytes() []byte { 29 | var buf bytes.Buffer 30 | header := s.MessageTypeIndicator // 0-1 bits 31 | if !s.MoreMessagesToSend { 32 | header |= 0x01 << 2 // 2 bit 33 | } 34 | if s.LoopPrevention { 35 | header |= 0x01 << 3 // 3 bit 36 | } 37 | if s.StatusReportQualificator { 38 | header |= 0x01 << 5 // 5 bit 39 | } 40 | if s.UserDataHeaderIndicator { 41 | header |= 0x01 << 6 // 6 bit 42 | } 43 | buf.WriteByte(header) 44 | buf.WriteByte(s.MessageReference) 45 | buf.Write(s.DestinationAddress) 46 | buf.Write(s.ServiceCentreTimestamp) 47 | buf.Write(s.DischargeTimestamp) 48 | buf.WriteByte(s.Status) 49 | 50 | var trailer bytes.Buffer 51 | var indicator byte 52 | if s.ProtocolIdentifier != 0 { 53 | indicator |= 0x01 << 0 // 0 bit 54 | trailer.WriteByte(s.ProtocolIdentifier) 55 | } 56 | if s.DataCodingScheme != 0 { 57 | indicator |= 0x01 << 1 // 1 bit 58 | trailer.WriteByte(s.DataCodingScheme) 59 | } 60 | if s.UserDataHeaderIndicator { 61 | indicator |= 0x01 << 2 // 2 bit 62 | trailer.WriteByte(s.UserDataLength) 63 | trailer.Write(s.UserData) 64 | } 65 | buf.WriteByte(indicator) 66 | if indicator != 0 { 67 | trailer.WriteTo(&buf) 68 | } 69 | return buf.Bytes() 70 | } 71 | 72 | func (s *smsStatusReport) FromBytes(octets []byte) (n int, err error) { //nolint:funlen 73 | buf := bytes.NewReader(octets) 74 | *s = smsStatusReport{} 75 | header, err := buf.ReadByte() 76 | n++ 77 | if err != nil { 78 | return 79 | } 80 | s.MessageTypeIndicator = header & 0x03 81 | if header>>2&0x01 == 0x00 { 82 | s.MoreMessagesToSend = true 83 | } 84 | if header>>3&0x01 == 0x01 { 85 | s.LoopPrevention = true 86 | } 87 | if header>>4&0x01 == 0x01 { 88 | s.StatusReportQualificator = true 89 | } 90 | s.UserDataHeaderIndicator = header&(0x01<<6) != 0 91 | 92 | s.MessageReference, err = buf.ReadByte() 93 | n++ 94 | if err != nil { 95 | return 96 | } 97 | 98 | daLen, err := buf.ReadByte() 99 | n++ 100 | if err != nil { 101 | return 102 | } 103 | if daLen > 16 { 104 | return n, ErrIncorrectSize 105 | } 106 | buf.UnreadByte() // will read length again 107 | n-- 108 | s.DestinationAddress = make([]byte, blocks(int(daLen), 2)+2) 109 | off, err := io.ReadFull(buf, s.DestinationAddress) 110 | n += off 111 | if err != nil { 112 | return 113 | } 114 | s.ServiceCentreTimestamp = make([]byte, 7) 115 | off, err = io.ReadFull(buf, s.ServiceCentreTimestamp) 116 | n += off 117 | if err != nil { 118 | return 119 | } 120 | s.DischargeTimestamp = make([]byte, 7) 121 | off, err = io.ReadFull(buf, s.DischargeTimestamp) 122 | n += off 123 | if err != nil { 124 | return 125 | } 126 | s.Status, err = buf.ReadByte() 127 | n++ 128 | if err != nil { 129 | return 130 | } 131 | s.Parameters, err = buf.ReadByte() 132 | n++ 133 | if err != nil { 134 | return n - 1, nil 135 | } 136 | if s.Parameters&0x01 != 0 { 137 | s.ProtocolIdentifier, err = buf.ReadByte() 138 | n++ 139 | if err != nil { 140 | return 141 | } 142 | } 143 | if s.Parameters&0x02 != 0 { 144 | s.DataCodingScheme, err = buf.ReadByte() 145 | n++ 146 | if err != nil { 147 | return 148 | } 149 | } 150 | if s.Parameters&0x04 != 0 { 151 | s.UserDataLength, err = buf.ReadByte() 152 | n++ 153 | if err != nil { 154 | return 155 | } 156 | s.UserData = make([]byte, int(s.UserDataLength)) 157 | off, _ = io.ReadFull(buf, s.UserData) 158 | s.UserData = s.UserData[:off] 159 | n += off 160 | } 161 | return n, err 162 | } 163 | -------------------------------------------------------------------------------- /sms/sms_submit.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // Low-level representation of an submit-type SMS message (3GPP TS 23.040). 9 | type smsSubmit struct { 10 | MessageTypeIndicator byte 11 | RejectDuplicates bool 12 | ValidityPeriodFormat byte 13 | ReplyPath bool 14 | UserDataHeaderIndicator bool 15 | StatusReportRequest bool 16 | 17 | MessageReference byte 18 | DestinationAddress []byte 19 | ProtocolIdentifier byte 20 | DataCodingScheme byte 21 | ValidityPeriod byte 22 | UserDataLength byte 23 | UserData []byte 24 | } 25 | 26 | func (s *smsSubmit) Bytes() []byte { 27 | var buf bytes.Buffer 28 | header := s.MessageTypeIndicator // 0-1 bits 29 | if s.RejectDuplicates { 30 | header |= 0x01 << 2 // 2 bit 31 | } 32 | header |= s.ValidityPeriodFormat << 3 // 3-4 bits 33 | if s.StatusReportRequest { 34 | header |= 0x01 << 5 // 5 bit 35 | } 36 | if s.UserDataHeaderIndicator { 37 | header |= 0x01 << 6 // 6 bit 38 | } 39 | if s.ReplyPath { 40 | header |= 0x01 << 7 // 7 bit 41 | } 42 | buf.WriteByte(header) 43 | buf.WriteByte(s.MessageReference) 44 | buf.Write(s.DestinationAddress) 45 | buf.WriteByte(s.ProtocolIdentifier) 46 | buf.WriteByte(s.DataCodingScheme) 47 | if ValidityPeriodFormat(s.ValidityPeriodFormat) != ValidityPeriodFormats.FieldNotPresent { 48 | buf.WriteByte(s.ValidityPeriod) 49 | } 50 | buf.WriteByte(s.UserDataLength) 51 | buf.Write(s.UserData) 52 | return buf.Bytes() 53 | } 54 | 55 | func (s *smsSubmit) FromBytes(octets []byte) (n int, err error) { //nolint:funlen 56 | *s = smsSubmit{} 57 | buf := bytes.NewReader(octets) 58 | header, err := buf.ReadByte() 59 | n++ 60 | if err != nil { 61 | return 62 | } 63 | s.MessageTypeIndicator = header & 0x03 64 | if header&(0x01<<2) > 0 { 65 | s.RejectDuplicates = true 66 | } 67 | s.ValidityPeriodFormat = header >> 3 & 0x03 68 | if header&(0x01<<5) > 0 { 69 | s.StatusReportRequest = true 70 | } 71 | if header&(0x01<<6) > 0 { 72 | s.UserDataHeaderIndicator = true 73 | } 74 | if header&(0x01<<7) > 0 { 75 | s.ReplyPath = true 76 | } 77 | s.MessageReference, err = buf.ReadByte() 78 | n++ 79 | if err != nil { 80 | return 81 | } 82 | daLen, err := buf.ReadByte() 83 | n++ 84 | if err != nil { 85 | return 86 | } 87 | if daLen > 16 { 88 | return n, ErrIncorrectSize 89 | } 90 | buf.UnreadByte() // read length again 91 | n-- 92 | s.DestinationAddress = make([]byte, blocks(int(daLen), 2)+2) 93 | off, err := io.ReadFull(buf, s.DestinationAddress) 94 | n += off 95 | if err != nil { 96 | return 97 | } 98 | s.ProtocolIdentifier, err = buf.ReadByte() 99 | n++ 100 | if err != nil { 101 | return 102 | } 103 | s.DataCodingScheme, err = buf.ReadByte() 104 | n++ 105 | if err != nil { 106 | return 107 | } 108 | if ValidityPeriodFormat(s.ValidityPeriodFormat) != ValidityPeriodFormats.FieldNotPresent { 109 | s.ValidityPeriod, err = buf.ReadByte() 110 | n++ 111 | if err != nil { 112 | return 113 | } 114 | } 115 | s.UserDataLength, err = buf.ReadByte() 116 | n++ 117 | if err != nil { 118 | return 119 | } 120 | s.UserData = make([]byte, int(s.UserDataLength)) 121 | off, _ = io.ReadFull(buf, s.UserData) 122 | s.UserData = s.UserData[:off] 123 | n += off 124 | return n, nil 125 | } 126 | -------------------------------------------------------------------------------- /sms/sms_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "github.com/xlab/at/util" 10 | ) 11 | 12 | var ( 13 | pduDeliverUCS2 = "07919761989901F0040B919762995696F000084160621263036178042D0442" + 14 | "043E0442002004300431043E043D0435043D0442002004370432043E043D0438043B0020043" + 15 | "20430043C0020003200200440043004370430002E0020041F043E0441043B04350434043D04" + 16 | "3804390020002D002000200032003600200438044E043D044F00200432002000320031003A0" + 17 | "0330035" 18 | pduSubmitUCS2 = "07919761989901F011000B919762995696F00008AA78042D0442043E04420020" + 19 | "04300431043E043D0435043D0442002004370432043E043D0438043B002004320430043C0020" + 20 | "003200200440043004370430002E0020041F043E0441043B04350434043D043804390020002D" + 21 | "002000200032003600200438044E043D044F00200432002000320031003A00330035" 22 | 23 | pduDeliverGsm7 = "07919762020033F1040B919762995696F0000041606291401561066379180E8200" 24 | pduSubmitGsm7 = "07919762020033F111000B919762995696F00000AA066379180E8200" 25 | pduDeliverGsm7_2 = "0791551010010201040D91551699296568F80011719022124215293DD4B71C5E26BF" + 26 | "41D3E6145476D3E5E573BD0C82BF40B59A2D96CBE564351BCE8603A164319D8CA6ABD540E432482673C172AED82DE502" 27 | 28 | pduStatusReport = "079194710600400706360d91947106000000f122206151457440222061514584400000" 29 | ) 30 | 31 | var ( 32 | smsDeliverUCS2 = Message{ 33 | Text: "Этот абонент звонил вам 2 раза. Последний - 26 июня в 21:35", 34 | Encoding: Encodings.UCS2, 35 | Type: MessageTypes.Deliver, 36 | Address: "+79269965690", 37 | ServiceCenterAddress: "+79168999100", 38 | ServiceCenterTime: parseTimestamp("2014-06-26T21:36:30+04:00"), 39 | } 40 | smsDeliverGsm7 = Message{ 41 | Text: "crap Δ", 42 | Encoding: Encodings.Gsm7Bit, 43 | Type: MessageTypes.Deliver, 44 | Address: "+79269965690", 45 | ServiceCenterAddress: "+79262000331", 46 | ServiceCenterTime: parseTimestamp("2014-06-26T19:04:51+04:00"), 47 | } 48 | smsDeliverGsm7_2 = Message{ 49 | Text: "Torpedo SMS entregue p/ 5561999256868 (21:24:55 de 22.09.17).", 50 | Encoding: Encodings.Gsm7Bit_2, 51 | Type: MessageTypes.Deliver, 52 | Address: "+5561999256868", 53 | ServiceCenterAddress: "+550101102010", 54 | ServiceCenterTime: parseTimestamp("2017-09-22T21:24:51-03:00"), 55 | } 56 | smsSubmitUCS2 = Message{ 57 | Text: "Этот абонент звонил вам 2 раза. Последний - 26 июня в 21:35", 58 | Encoding: Encodings.UCS2, 59 | Type: MessageTypes.Submit, 60 | Address: "+79269965690", 61 | ServiceCenterAddress: "+79168999100", 62 | VP: ValidityPeriod(time.Hour * 24 * 4), 63 | VPFormat: ValidityPeriodFormats.Relative, 64 | } 65 | smsSubmitGsm7 = Message{ 66 | Text: "crap Δ", 67 | Encoding: Encodings.Gsm7Bit, 68 | Type: MessageTypes.Submit, 69 | Address: "+79269965690", 70 | ServiceCenterAddress: "+79262000331", 71 | VP: ValidityPeriod(time.Hour * 24 * 4), 72 | VPFormat: ValidityPeriodFormats.Relative, 73 | } 74 | smsReport = Message{ 75 | Type: MessageTypes.StatusReport, 76 | Status: 0x00, // received 77 | MessageReference: 54, 78 | Address: "+4917600000001", 79 | ServiceCenterAddress: "+491760000470", 80 | ServiceCenterTime: parseTimestamp("2022-02-16T15:54:47+01:00"), 81 | DischargeTime: parseTimestamp("2022-02-16T15:54:48+01:00"), 82 | } 83 | ) 84 | 85 | // parseTimestamp, a test helper, parses an RFC3339-formatted date into 86 | // a Timestamp. If the input is malformed, parseTimestamp panics. 87 | func parseTimestamp(timetamp string) Timestamp { 88 | date, err := time.Parse(time.RFC3339, timetamp) 89 | if err != nil { 90 | panic(err) 91 | } 92 | return Timestamp(date) 93 | } 94 | 95 | func TestSmsDeliverReadFromUCS2(t *testing.T) { 96 | t.Parallel() 97 | 98 | var msg Message 99 | data, err := util.Bytes(pduDeliverUCS2) 100 | require.NoError(t, err) 101 | n, err := msg.ReadFrom(data) 102 | require.NoError(t, err) 103 | assert.Equal(t, n, len(data)) 104 | assert.Equal(t, smsDeliverUCS2, msg) 105 | } 106 | 107 | func TestSmsDeliverReadFromGsm7(t *testing.T) { 108 | t.Parallel() 109 | 110 | var msg Message 111 | data, err := util.Bytes(pduDeliverGsm7) 112 | require.NoError(t, err) 113 | n, err := msg.ReadFrom(data) 114 | require.NoError(t, err) 115 | assert.Equal(t, n, len(data)) 116 | assert.Equal(t, smsDeliverGsm7, msg) 117 | } 118 | 119 | func TestSmsDeliverReadFromGsm7_2(t *testing.T) { 120 | t.Parallel() 121 | 122 | var msg Message 123 | data, err := util.Bytes(pduDeliverGsm7_2) 124 | require.NoError(t, err) 125 | n, err := msg.ReadFrom(data) 126 | require.NoError(t, err) 127 | assert.Equal(t, n, len(data)) 128 | assert.Equal(t, smsDeliverGsm7_2, msg) 129 | } 130 | 131 | func TestSmsDeliverPduUCS2(t *testing.T) { 132 | t.Parallel() 133 | 134 | n, octets, err := smsDeliverUCS2.PDU() 135 | require.NoError(t, err) 136 | assert.Equal(t, len(pduDeliverUCS2)/2-8, n) 137 | data, err := util.Bytes(pduDeliverUCS2) 138 | require.NoError(t, err) 139 | assert.Equal(t, data, octets) 140 | } 141 | 142 | func TestSmsDeliverPduGsm7(t *testing.T) { 143 | t.Parallel() 144 | 145 | n, octets, err := smsDeliverGsm7.PDU() 146 | require.NoError(t, err) 147 | assert.Equal(t, len(pduDeliverGsm7)/2-8, n) 148 | data, err := util.Bytes(pduDeliverGsm7) 149 | t.Logf("%02x\n", string(data)) 150 | t.Logf("%02x\n", string(octets)) 151 | require.NoError(t, err) 152 | assert.Equal(t, data, octets) 153 | } 154 | 155 | func TestSmsSubmitReadFromUCS2(t *testing.T) { 156 | t.Parallel() 157 | 158 | var msg Message 159 | data, err := util.Bytes(pduSubmitUCS2) 160 | require.NoError(t, err) 161 | n, err := msg.ReadFrom(data) 162 | require.NoError(t, err) 163 | assert.Equal(t, n, len(data)) 164 | assert.Equal(t, smsSubmitUCS2, msg) 165 | } 166 | 167 | func TestSmsSubmitReadFromGsm7(t *testing.T) { 168 | t.Parallel() 169 | 170 | var msg Message 171 | data, err := util.Bytes(pduSubmitGsm7) 172 | require.NoError(t, err) 173 | n, err := msg.ReadFrom(data) 174 | require.NoError(t, err) 175 | assert.Equal(t, n, len(data)) 176 | assert.Equal(t, smsSubmitGsm7, msg) 177 | } 178 | 179 | func TestSmsSubmitPduUCS2(t *testing.T) { 180 | t.Parallel() 181 | 182 | n, octets, err := smsSubmitUCS2.PDU() 183 | require.NoError(t, err) 184 | assert.Equal(t, len(pduSubmitUCS2)/2-8, n) 185 | data, err := util.Bytes(pduSubmitUCS2) 186 | require.NoError(t, err) 187 | assert.Equal(t, data, octets) 188 | } 189 | 190 | func TestSmsSubmitPduGsm7(t *testing.T) { 191 | t.Parallel() 192 | 193 | n, octets, err := smsSubmitGsm7.PDU() 194 | require.NoError(t, err) 195 | assert.Equal(t, len(pduSubmitGsm7)/2-8, n) 196 | data, err := util.Bytes(pduSubmitGsm7) 197 | require.NoError(t, err) 198 | assert.Equal(t, data, octets) 199 | } 200 | 201 | func TestSmsStatusReport(t *testing.T) { 202 | t.Parallel() 203 | 204 | m := Message{} 205 | _, err := m.ReadFrom(util.MustBytes(pduStatusReport)) 206 | require.NoError(t, err) 207 | 208 | n, octets, err := smsReport.PDU() 209 | require.NoError(t, err) 210 | assert.Equal(t, len(pduStatusReport)/2-8, n) 211 | data, err := util.Bytes(pduStatusReport) 212 | require.NoError(t, err) 213 | assert.Equal(t, data, octets) 214 | } 215 | -------------------------------------------------------------------------------- /sms/status.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | // StatusCategory 4 | type StatusCategory byte 5 | 6 | var StatusCategories = struct { 7 | Complete StatusCategory // Short message transaction completed 8 | TemporaryError StatusCategory // Temporary error, SC still trying to transfer SM 9 | PermanentError StatusCategory // Permanent error, SC is not making any more transfer attempts 10 | FinalError StatusCategory // Temporary error, SC is not making any more transfer attempts 11 | 12 | Unknown StatusCategory // Status code is either reserved or SC-specific 13 | }{ 14 | 0x00, 0x01, 0x02, 0x03, 15 | 0x80, // reserved 16 | } 17 | 18 | // Status represents the status of a SMS-STATUS-REPORT TPDU, as specified 19 | // in 3GPP TS 23.040 version 16.0.0 release 16, section 9.2.3.15. 20 | type Status byte 21 | 22 | // Catogory returns the kind of status, laid out in 3GPP TS 23.040 version 23 | // 16.0.0 release 16, section 9.2.3.15. 24 | // 25 | // If s represents a reserved or Service Centre-specific status, Category 26 | // will return StatusCategories.Unknown. 27 | func (s Status) Category() StatusCategory { 28 | switch { 29 | case 0b0000_0011 <= s && s <= 0b0001_1111, 30 | 0b0010_0110 <= s && s <= 0b0011_1111, 31 | 0b0100_1010 <= s && s <= 0b0101_1111, 32 | 0b0110_0110 <= s && s <= 0b1111_1111: 33 | return StatusCategories.Unknown 34 | default: 35 | // category is encoded in bits 6 and 5 36 | return StatusCategory(s >> 5 & 0x03) 37 | } 38 | } 39 | 40 | // StatusCodes represents possible values for the Status field in 41 | // SMS-STATUS-REPORT TPDUs. 42 | var StatusCodes = struct { 43 | // Transaction complete status codes 44 | CompletedReceived Status 45 | CompletedForwared Status 46 | CompletedReplaced Status 47 | 48 | // Temporary error, service center still tries delivery 49 | TemporaryCongestion Status 50 | TemporaryBusy Status 51 | TemporaryNoResponseFromRecipient Status 52 | TemporaryServiceRejected Status 53 | TemporaryQualityOfServiceNotAvailable Status 54 | TemporaryErrorInRecipient Status 55 | 56 | // Permanent error, SC is not making any more transfer attempts 57 | PermanentRemoteProcedureError Status 58 | PermanentIncompatibleDestination Status 59 | PermanentConnectionRejected Status 60 | PermanentNotObtainable Status 61 | PermanentQualityOfServiceNotAvailable Status 62 | PermanentNoInterworkingAvailable Status 63 | PermanentValidityPeriodExpired Status 64 | PermanentDeletedBeSender Status 65 | PermanentDeletedByAdministration Status 66 | PermanentUnknownMessage Status 67 | 68 | // Temporary error, SC is not making any more transfer attempts 69 | FinalCongestion Status 70 | FinalBusy Status 71 | FinalNoResponseFromRecipient Status 72 | FinalServiceRejected Status 73 | FinalQualityOfServiceNotAvailable Status 74 | FinalErrorInRecipient Status 75 | }{ 76 | 0b0000_0000, // Short message received by the SME 77 | 0b0000_0001, // Short message forwarded by the SC to the SME but the SC is unable to confirm delivery 78 | 0b0000_0010, // Short message replaced by the SC 79 | // 0000 0011 .. 0000 1111 // Reserved 80 | // 0001 0000 .. 0001 1111 // Values specific to each SC 81 | 82 | 0b0010_0000, // Congestion 83 | 0b0010_0001, // SME busy 84 | 0b0010_0010, // No response from SME 85 | 0b0010_0011, // Service rejected 86 | 0b0010_0100, // Quality of service not available 87 | 0b0010_0101, // Error in SME 88 | // 0010 0110 .. 0010 1111 // Reserved 89 | // 0011 0000 .. 0011 1111 // Values specific to each SC 90 | 91 | 0b0100_0000, // Remote procedure error 92 | 0b0100_0001, // Incompatible destination 93 | 0b0100_0010, // Connection rejected by SME 94 | 0b0100_0011, // Not obtainable 95 | 0b0100_0100, // Quality of service not available 96 | 0b0100_0101, // No interworking available 97 | 0b0100_0110, // SM Validity Period Expired 98 | 0b0100_0111, // SM Deleted by originating SME 99 | 0b0100_1000, // SM Deleted by SC Administration 100 | 0b0100_1001, // SM does not exist (The SM may have previously existed in the SC but the SC no longer has knowledge of it or the SM may never have previously existed in the SC) 101 | // 0100 1010 .. 0100 1111 // Reserved 102 | // 0101 0000 .. 0101 1111 // Values specific to each SC 103 | 104 | 0b0110_0000, // Congestion 105 | 0b0110_0001, // SME busy 106 | 0b0110_0010, // No response from SME 107 | 0b0110_0011, // Service rejected 108 | 0b0110_0100, // Quality of service not available 109 | 0b0110_0101, // Error in SME 110 | // 0110 0110 .. 0110 1001 // Reserved 111 | // 0110 1010 .. 0110 1111 // Reserved 112 | // 0111 0000 .. 0111 1111 // Values specific to each SC 113 | 114 | // 1000 0000 .. 1111 1111 // reserved 115 | } 116 | -------------------------------------------------------------------------------- /sms/status_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestStatusCategorys(t *testing.T) { 10 | t.Parallel() 11 | run := func(t *testing.T, name string, cat StatusCategory, s []Status) { 12 | t.Helper() 13 | t.Run(name, func(t *testing.T) { 14 | t.Parallel() 15 | for _, status := range s { 16 | actual := status.Category() 17 | assert.Equal(t, cat, actual) 18 | } 19 | }) 20 | } 21 | run(t, "complete", StatusCategories.Complete, []Status{ 22 | StatusCodes.CompletedReceived, 23 | StatusCodes.CompletedForwared, 24 | StatusCodes.CompletedReplaced, 25 | }) 26 | run(t, "temporary", StatusCategories.TemporaryError, []Status{ 27 | StatusCodes.TemporaryCongestion, 28 | StatusCodes.TemporaryBusy, 29 | StatusCodes.TemporaryNoResponseFromRecipient, 30 | StatusCodes.TemporaryServiceRejected, 31 | StatusCodes.TemporaryQualityOfServiceNotAvailable, 32 | StatusCodes.TemporaryErrorInRecipient, 33 | }) 34 | run(t, "permanent", StatusCategories.PermanentError, []Status{ 35 | StatusCodes.PermanentRemoteProcedureError, 36 | StatusCodes.PermanentIncompatibleDestination, 37 | StatusCodes.PermanentConnectionRejected, 38 | StatusCodes.PermanentNotObtainable, 39 | StatusCodes.PermanentQualityOfServiceNotAvailable, 40 | StatusCodes.PermanentNoInterworkingAvailable, 41 | StatusCodes.PermanentValidityPeriodExpired, 42 | StatusCodes.PermanentDeletedBeSender, 43 | StatusCodes.PermanentDeletedByAdministration, 44 | StatusCodes.PermanentUnknownMessage, 45 | }) 46 | run(t, "final", StatusCategories.FinalError, []Status{ 47 | StatusCodes.FinalCongestion, 48 | StatusCodes.FinalBusy, 49 | StatusCodes.FinalNoResponseFromRecipient, 50 | StatusCodes.FinalServiceRejected, 51 | StatusCodes.FinalQualityOfServiceNotAvailable, 52 | StatusCodes.FinalErrorInRecipient, 53 | }) 54 | t.Run("unknown", func(t *testing.T) { 55 | for _, ranges := range []struct{ begin, end byte }{ 56 | {0b0000_0011, 0b0000_1111}, // complete: Reserved 57 | {0b0001_0000, 0b0001_1111}, // complete: Values specific to each SC 58 | {0b0010_0110, 0b0010_1111}, // temporary: Reserved 59 | {0b0011_0000, 0b0011_1111}, // temporary: Values specific to each SC 60 | {0b0100_1010, 0b0100_1111}, // permanent: Reserved 61 | {0b0101_0000, 0b0101_1111}, // permanent: Values specific to each SC 62 | {0b0110_0110, 0b0110_1001}, // final: Reserved 63 | {0b0110_1010, 0b0110_1111}, // final: Reserved 64 | {0b0111_0000, 0b0111_1111}, // final: Values specific to each SC 65 | {0b1000_0000, 0b1111_1111}, // extension: reserved 66 | } { 67 | for i := ranges.begin; i < ranges.end; i++ { 68 | actual := Status(i).Category() 69 | assert.Equal(t, StatusCategories.Unknown, actual, "Status(%08b)", i) 70 | } 71 | } 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /sms/timestamp.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/xlab/at/pdu" 7 | ) 8 | 9 | // Timestamp represents message's timestamp. 10 | type Timestamp time.Time 11 | 12 | // PDU returns bytes of semi-octet encoded timestamp, as specified in 13 | // 3GPP TS 23.040 version 16.0.0 release 16, section 9.2.3.11. 14 | // 15 | // TP-Service-Centre-Time-Stamp (TP-SCTS) 16 | // 17 | // | | Year | Month | Day | Hour | Minute | Second | Time Zone | 18 | // |-------------|------|-------|-----|------|--------|--------|-----------| 19 | // | Semi-octets | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 20 | // 21 | // The Time Zone indicates the difference, expressed in quarters of an hour, 22 | // between the local time and GMT. In the first of the two semi-octets, the 23 | // first bit (bit 3 of the seventh octet of the TP-Service-CentreTime-Stamp 24 | // field) represents the algebraic sign of this difference (0: positive, 25 | // 1: negative). 26 | func (t Timestamp) PDU() []byte { 27 | date := time.Time(t) 28 | year, month, day := date.Date() 29 | hour, minute, second := date.Clock() 30 | 31 | _, offset := date.Zone() 32 | negativeOffset := offset < 0 //nolint:ifshort // false positive 33 | if negativeOffset { 34 | offset = -offset 35 | } 36 | 37 | const secPerQuarter = int(15 * time.Minute / time.Second) 38 | quarters := offset / secPerQuarter 39 | 40 | octets := []byte{ 41 | /* YY */ pdu.Swap(pdu.Encode(year % 1000)), 42 | /* MM */ pdu.Swap(pdu.Encode(int(month))), 43 | /* DD */ pdu.Swap(pdu.Encode(day)), 44 | /* hh */ pdu.Swap(pdu.Encode(hour)), 45 | /* mm */ pdu.Swap(pdu.Encode(minute)), 46 | /* ss */ pdu.Swap(pdu.Encode(second)), 47 | /* zz */ pdu.Swap(pdu.Encode(quarters)), 48 | } 49 | if negativeOffset { 50 | octets[6] |= 0x08 51 | } 52 | 53 | return octets 54 | } 55 | 56 | // ReadFrom reads a semi-encoded timestamp from the given octets. 57 | // See (*Timestamp).PDU() for format details. 58 | func (t *Timestamp) ReadFrom(octets []byte) { 59 | millennium := (time.Now().Year() / 1000) * 1000 60 | year := pdu.Decode(pdu.Swap(octets[0])) 61 | month := pdu.Decode(pdu.Swap(octets[1])) 62 | day := pdu.Decode(pdu.Swap(octets[2])) 63 | hour := pdu.Decode(pdu.Swap(octets[3])) 64 | minute := pdu.Decode(pdu.Swap(octets[4])) 65 | second := pdu.Decode(pdu.Swap(octets[5])) 66 | 67 | negativeOffset := (octets[6] & 0x08) != 0 68 | quarters := pdu.Decode(pdu.Swap(octets[6] & 0xF7)) 69 | offset := time.Duration(quarters) * 15 * time.Minute 70 | 71 | date := time.Date(millennium+year, time.Month(month), day, hour, minute, second, 0, time.UTC) 72 | 73 | if negativeOffset { 74 | offset = -offset 75 | } 76 | date = date.Add(-offset).In(time.FixedZone("", int(offset.Seconds()))) 77 | *t = Timestamp(date) 78 | } 79 | -------------------------------------------------------------------------------- /sms/timestamp_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/xlab/at/util" 9 | ) 10 | 11 | func TestTimestamp_PDU(t *testing.T) { 12 | t.Parallel() 13 | 14 | for _, tc := range []struct { 15 | date string 16 | expected string 17 | }{ 18 | {"2021-03-04T05:06:07+08:15", "12304050607033"}, 19 | {"2021-03-04T05:06:07-08:15", "1230405060703B"}, 20 | {"2000-01-01T00:00:00Z", "00101000000000"}, 21 | {"1999-12-31T23:59:59Z", "99211332959500"}, 22 | } { 23 | ts := parseTimestamp(tc.date) 24 | actual := util.HexString(ts.PDU()) 25 | assert.Equal(t, tc.expected, actual) 26 | } 27 | } 28 | 29 | func TestTimestamp_ReadFrom(t *testing.T) { 30 | t.Parallel() 31 | 32 | for _, tc := range []struct { 33 | pdu string 34 | expected string 35 | }{ 36 | {"12304050607023", "2021-03-04T05:06:07+08:00"}, 37 | {"12304050607033", "2021-03-04T05:06:07+08:15"}, 38 | {"1230405060703B", "2021-03-04T05:06:07-08:15"}, 39 | {"00101000000000", "2000-01-01T00:00:00Z"}, 40 | {"99211332959500", "2099-12-31T23:59:59Z"}, // [sic] 41 | } { 42 | var subject Timestamp 43 | pdu := util.MustBytes(tc.pdu) 44 | subject.ReadFrom(pdu) 45 | assert.Equal(t, tc.expected, time.Time(subject).Format(time.RFC3339)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sms/user_data_header.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | type UserDataHeader struct { 4 | TotalNumber int 5 | Sequence int 6 | Tag int 7 | } 8 | 9 | func (udh *UserDataHeader) ReadFrom(octets []byte) error { 10 | octetsLng := len(octets) 11 | headerLng := int(octets[0]) + 1 12 | if (octetsLng-headerLng) <= 0 || headerLng <= 5 { 13 | return ErrIncorrectUserDataHeaderLength 14 | } 15 | 16 | h := octets[:headerLng] 17 | udh.Sequence = int(h[5]) 18 | udh.TotalNumber = int(h[4]) 19 | udh.Tag = int(h[3]) 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /sms/ussd.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import "github.com/xlab/at/pdu" 4 | 5 | // USSD represents an USSD query string. 6 | type USSD string 7 | 8 | // Gsm7Bit encodes USSD query into GSM 7-Bit packed octets. 9 | func (u USSD) Gsm7Bit() []byte { 10 | return pdu.Encode7Bit(string(u)) 11 | } 12 | -------------------------------------------------------------------------------- /sms/validity_period.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import "time" 4 | 5 | // ValidityPeriodFormat represents the format of message's validity period. 6 | type ValidityPeriodFormat byte 7 | 8 | // ValidityPeriodFormats represent the possible formats of message's 9 | // validity period (3GPP TS 23.040). 10 | var ValidityPeriodFormats = struct { 11 | FieldNotPresent ValidityPeriodFormat 12 | Relative ValidityPeriodFormat 13 | Enhanced ValidityPeriodFormat 14 | Absolute ValidityPeriodFormat 15 | }{ 16 | 0x00, 0x02, 0x01, 0x03, 17 | } 18 | 19 | // ValidityPeriod represents the validity period of message. 20 | type ValidityPeriod time.Duration 21 | 22 | // Octet return a one-byte representation of the validity period. 23 | func (v ValidityPeriod) Octet() byte { 24 | switch d := time.Duration(v); { 25 | case d/time.Minute < 5: 26 | return 0x00 27 | case d/time.Hour < 12: 28 | return byte(d / (time.Minute * 5)) 29 | case d/time.Hour < 24: 30 | return byte((d-d/time.Hour*12)/(time.Minute*30) + 143) 31 | case d/time.Hour < 744: 32 | days := d / (time.Hour * 24) 33 | return byte(days + 166) 34 | default: 35 | weeks := d / (time.Hour * 24 * 7) 36 | if weeks > 62 { 37 | return 0xFF 38 | } 39 | return byte(weeks + 192) 40 | } 41 | } 42 | 43 | // ReadFrom reads the validity period form the given byte. 44 | func (v *ValidityPeriod) ReadFrom(oct byte) { 45 | switch n := time.Duration(oct); { 46 | case n >= 0 && n <= 143: 47 | *v = ValidityPeriod(5 * time.Minute * n) 48 | case n >= 144 && n <= 167: 49 | *v = ValidityPeriod(12*time.Hour + 30*time.Minute*(n-143)) 50 | case n >= 168 && n <= 196: 51 | *v = ValidityPeriod(24 * time.Hour * (n - 166)) 52 | case n >= 197 && n <= 255: 53 | *v = ValidityPeriod(7 * 24 * time.Hour * (n - 192)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | // Package util provides some useful methods that are used within the 'at' package. These methods include a method to 2 | // extract bytes from a string. 3 | package util 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "strconv" 9 | ) 10 | 11 | // Common errors. 12 | var ( 13 | ErrUnevenLength = errors.New("parse octets: uneven length of string") 14 | ErrUnexpected = errors.New("parse octets: met a non-HEX rune in string") 15 | ) 16 | 17 | // Bytes parses the hex-string of odd length into bytes. 18 | func Bytes(hex string) ([]byte, error) { 19 | if len(hex)%2 != 0 { 20 | return nil, ErrUnevenLength 21 | } 22 | octets := make([]byte, 0, len(hex)/2) 23 | for i := 0; i < len(hex); i += 2 { 24 | frame := hex[i : i+2] 25 | oct, err := strconv.ParseUint(frame, 16, 8) 26 | if err != nil { 27 | return nil, ErrUnexpected 28 | } 29 | octets = append(octets, byte(oct)) 30 | } 31 | return octets, nil 32 | } 33 | 34 | // MustBytes is an alias for Bytes, except that it will panic 35 | // if there is any parse error. 36 | func MustBytes(hex string) []byte { 37 | b, err := Bytes(hex) 38 | if err != nil { 39 | panic(err) 40 | } 41 | return b 42 | } 43 | 44 | // HexString produces a hex-string from bytes. Like a DEADBEEF, without prepending the 0x. 45 | func HexString(octets []byte) string { 46 | return fmt.Sprintf("%2X", octets) 47 | } 48 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBytes(t *testing.T) { 10 | t.Parallel() 11 | 12 | out, err := Bytes("4160629140050E") 13 | exp := []byte{0x41, 0x60, 0x62, 0x91, 0x40, 0x5, 0x0e} 14 | assert.NoError(t, err) 15 | assert.Equal(t, exp, out) 16 | 17 | _, err = Bytes("4160629140050E0") 18 | assert.EqualError(t, err, "parse octets: uneven length of string") 19 | 20 | _, err = Bytes("4160629140050K") 21 | assert.EqualError(t, err, "parse octets: met a non-HEX rune in string") 22 | } 23 | 24 | func TestHexString(t *testing.T) { 25 | t.Parallel() 26 | 27 | buf := []byte{0x41, 0x60, 0x62, 0x91, 0x40, 0x5, 0x0e} 28 | out := HexString(buf) 29 | exp := "4160629140050E" 30 | assert.Equal(t, exp, out) 31 | } 32 | --------------------------------------------------------------------------------