├── .gitignore ├── static ├── gohtml │ ├── footer.gohtml │ ├── login.gohtml │ ├── index.gohtml │ ├── send_sms.gohtml │ ├── history.gohtml │ └── header.gohtml ├── img │ └── favicon.ico └── tpl.go ├── model ├── ack.go ├── msg.go └── sms.go ├── README.md ├── config ├── config.go └── model.go ├── db ├── init.go ├── db.go └── history.go ├── go.mod ├── app ├── app.go ├── global.go ├── session.go └── handler.go ├── serial ├── map.go ├── serial_us.go └── serial_cn.go ├── config.ini ├── LICENSE ├── main.go ├── go.sum └── air780e └── main.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /static/gohtml/footer.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "footer" }} 2 | 3 | 4 | {{ end }} -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akvicor/sms/HEAD/static/img/favicon.ico -------------------------------------------------------------------------------- /model/ack.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "encoding/json" 4 | 5 | type ACK struct { 6 | Key string `json:"key"` 7 | } 8 | 9 | func UnmarshalACK(data []byte) *ACK { 10 | msg := &ACK{} 11 | err := json.Unmarshal(data, msg) 12 | if err != nil { 13 | return nil 14 | } 15 | return msg 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SMS 2 | 3 | 树莓派与Air780E搭建的短信收发平台 4 | 5 | ``` 6 | GET: 7 | /random_key?range=(不提供则使用默认值)&length=(默认为8) 8 | POST: 9 | /send_sms?key=(访问密钥,如果已通过网页登录则不需要)&sender=(发送者)&phone=(手机号)&message=(短信内容) 10 | ``` 11 | 12 | 具体配置信息在config.ini中 13 | 14 | ## 如果想搭建, 请先看完此篇博客, 博客中有详细的说明和所需材料 15 | 16 | [https://blog.akvicor.com/posts/project/sms](https://blog.akvicor.com/posts/project/sms/) 17 | -------------------------------------------------------------------------------- /static/gohtml/login.gohtml: -------------------------------------------------------------------------------- 1 | {{ template "header" . }} 2 |
3 |
4 |
5 | 8 | 11 | 12 |
13 |
14 |
15 | {{ template "footer" . }} -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/Akvicor/glog" 5 | "github.com/go-ini/ini" 6 | ) 7 | 8 | var cfg *ini.File 9 | var Global *Model 10 | 11 | func Load(path string) { 12 | var err error 13 | cfg, err = ini.Load(path) 14 | if err != nil { 15 | glog.Fatal("unable to read config [%s][%s]", path, err.Error()) 16 | } 17 | Global = new(Model) 18 | err = cfg.MapTo(Global) 19 | if err != nil { 20 | glog.Fatal("unable to parse config [%s]", err.Error()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /db/init.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/Akvicor/glog" 5 | "github.com/Akvicor/util" 6 | "sms/config" 7 | ) 8 | 9 | func CreateDatabase() { 10 | if util.FileStat(config.Global.Database.Path).IsExist() { 11 | glog.Fatal("database file exist!") 12 | } 13 | d := Connect() 14 | if d == nil { 15 | glog.Fatal("con not connect to database!") 16 | } 17 | err := db.AutoMigrate(&HistoryModel{}) 18 | if err != nil { 19 | glog.Fatal(err.Error()) 20 | } 21 | glog.Info("database create finished") 22 | } 23 | -------------------------------------------------------------------------------- /static/gohtml/index.gohtml: -------------------------------------------------------------------------------- 1 | {{ template "header" . }} 2 | 3 |
4 |
5 |
6 |

7 |

8 |

9 |

10 |
11 |
12 |
13 | 14 | {{ template "footer" . }} -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sms 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/Akvicor/glog v0.2.3 7 | github.com/Akvicor/protocol v0.2.3 8 | github.com/Akvicor/util v1.10.7 9 | github.com/go-ini/ini v1.67.0 10 | github.com/gorilla/securecookie v1.1.2 11 | github.com/gorilla/sessions v1.2.2 12 | github.com/patrickmn/go-cache v2.1.0+incompatible 13 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 14 | gorm.io/driver/sqlite v1.5.4 15 | gorm.io/gorm v1.25.6 16 | ) 17 | 18 | require ( 19 | github.com/jinzhu/inflection v1.0.0 // indirect 20 | github.com/jinzhu/now v1.1.5 // indirect 21 | github.com/mattn/go-sqlite3 v1.14.17 // indirect 22 | github.com/stretchr/testify v1.8.4 // indirect 23 | golang.org/x/sys v0.15.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/Akvicor/glog" 5 | "gorm.io/driver/sqlite" 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | "sms/config" 9 | "sync" 10 | ) 11 | 12 | var ( 13 | db *gorm.DB 14 | dbLock = sync.RWMutex{} 15 | connected = false 16 | ) 17 | 18 | func Connect() *gorm.DB { 19 | if connected { 20 | return db 21 | } 22 | dbLock.Lock() 23 | defer dbLock.Unlock() 24 | if connected { 25 | return db 26 | } 27 | var err error 28 | db, err = gorm.Open(sqlite.Open(config.Global.Database.Path), &gorm.Config{ 29 | Logger: logger.Default.LogMode(logger.Silent), 30 | }) 31 | if err != nil { 32 | glog.Warning("failed to connect database [%s]", err.Error()) 33 | return nil 34 | } 35 | connected = true 36 | return db 37 | } 38 | -------------------------------------------------------------------------------- /static/gohtml/send_sms.gohtml: -------------------------------------------------------------------------------- 1 | {{ template "header" . }} 2 | 3 |
4 |
5 |
6 |


7 | 10 | 13 | 16 | 17 |
18 |
19 |
20 | 21 | {{ template "footer" . }} -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/Akvicor/glog" 5 | "github.com/Akvicor/util" 6 | "github.com/gorilla/sessions" 7 | "net/http" 8 | "sync" 9 | ) 10 | 11 | type app struct { 12 | mutex sync.RWMutex 13 | session *sessions.CookieStore 14 | handler map[string]func(w http.ResponseWriter, r *http.Request) 15 | } 16 | 17 | func (a *app) ServeHTTP(w http.ResponseWriter, r *http.Request) { 18 | head, tail := util.SplitPath(r.URL.Path) 19 | glog.Debug("[%-4s][%-32s] [%s][%s]", r.Method, "/", head, tail) 20 | 21 | var handler func(w http.ResponseWriter, r *http.Request) 22 | var ok bool 23 | 24 | handler, ok = a.handler[head] 25 | if ok { 26 | handler(w, r) 27 | return 28 | } 29 | 30 | glog.Debug("Unhandled [%-4s][%-32s] [%s][%s]", r.Method, "/", head, tail) 31 | } 32 | -------------------------------------------------------------------------------- /serial/map.go: -------------------------------------------------------------------------------- 1 | package serial 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type SyncMap struct { 8 | mp map[string]chan struct{} 9 | lock sync.Mutex 10 | } 11 | 12 | func NewSyncMap() *SyncMap { 13 | return &SyncMap{ 14 | mp: make(map[string]chan struct{}), 15 | lock: sync.Mutex{}, 16 | } 17 | } 18 | 19 | func (s *SyncMap) Put(key string) chan struct{} { 20 | s.lock.Lock() 21 | defer s.lock.Unlock() 22 | c := make(chan struct{}, 1) 23 | s.mp[key] = c 24 | return c 25 | } 26 | 27 | func (s *SyncMap) Trick(key string) { 28 | s.lock.Lock() 29 | defer s.lock.Unlock() 30 | _, ok := s.mp[key] 31 | if ok { 32 | s.mp[key] <- struct{}{} 33 | } 34 | } 35 | 36 | func (s *SyncMap) Delete(key string) { 37 | s.lock.Lock() 38 | defer s.lock.Unlock() 39 | _, ok := s.mp[key] 40 | if ok { 41 | delete(s.mp, key) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | brand_name = SMS Pusher 2 | prod = false 3 | 4 | # TXD GPIO 8 5 | # RXD GPIO 9 6 | [serial-cn] 7 | name = /dev/ttyAMA4 8 | baud = 115200 9 | send_queue_size = 8 10 | heartbeat_send_interval = 15 11 | heartbeat_receive_timeout = 40 12 | 13 | # TXD GPIO 12 14 | # RXD GPIO 13 15 | [serial-us] 16 | name = /dev/ttyAMA5 17 | baud = 115200 18 | send_queue_size = 8 19 | heartbeat_send_interval = 15 20 | heartbeat_receive_timeout = 40 21 | 22 | [server] 23 | http_addr = 0.0.0.0 24 | http_port = 8080 25 | enable_https = false 26 | ssl_cert = server.crt 27 | ssl_key = server.key 28 | 29 | [session] 30 | domain = 31 | path = / 32 | name = sms 33 | max_age = 604800 34 | 35 | [database] 36 | path = /path/to/db/sms.db 37 | 38 | [log] 39 | log_to_file = false 40 | file_path = /path/to/log/sms.log 41 | 42 | [security] 43 | username = Akvicor 44 | password = password 45 | access_key = key 46 | -------------------------------------------------------------------------------- /static/gohtml/history.gohtml: -------------------------------------------------------------------------------- 1 | {{ template "header" . }} 2 | 3 |
4 |
5 |
6 |


7 | {{ range .histories }} 8 | 16 | {{ else }} 17 |

18 | {{ end}} 19 |
20 |
21 |
22 | 23 | {{ template "footer" . }} -------------------------------------------------------------------------------- /static/tpl.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "embed" 5 | _ "embed" 6 | "github.com/Akvicor/glog" 7 | "html/template" 8 | ) 9 | 10 | //go:embed img/favicon.ico 11 | var Favicon []byte 12 | 13 | //go:embed gohtml/* 14 | var html embed.FS 15 | 16 | var Login *template.Template 17 | var Index *template.Template 18 | var SendSMS *template.Template 19 | var History *template.Template 20 | 21 | func init() { 22 | t := template.Must(template.ParseFS(html, "gohtml/*")) 23 | 24 | Login = t.Lookup("login.gohtml") 25 | if Login == nil { 26 | glog.Fatal("missing gohtml template [login.gohtml]") 27 | } 28 | Index = t.Lookup("index.gohtml") 29 | if Index == nil { 30 | glog.Fatal("missing gohtml template [index.gohtml]") 31 | } 32 | SendSMS = t.Lookup("send_sms.gohtml") 33 | if SendSMS == nil { 34 | glog.Fatal("missing gohtml template [send_sms.gohtml]") 35 | } 36 | History = t.Lookup("history.gohtml") 37 | if History == nil { 38 | glog.Fatal("missing gohtml template [history.gohtml]") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Akvicor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/global.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/gorilla/securecookie" 5 | "github.com/gorilla/sessions" 6 | "net/http" 7 | "sms/config" 8 | "sync" 9 | ) 10 | 11 | var Global *app 12 | 13 | func Generate() { 14 | Global = new(app) 15 | Global.session = sessions.NewCookieStore(securecookie.GenerateRandomKey(32)) 16 | Global.session.Options.Domain = config.Global.Session.Domain 17 | Global.session.Options.Path = config.Global.Session.Path 18 | Global.session.Options.MaxAge = config.Global.Session.MaxAge 19 | Global.handler = make(map[string]func(w http.ResponseWriter, r *http.Request)) 20 | Global.mutex = sync.RWMutex{} 21 | 22 | Global.handler["/favicon.ico"] = staticFavicon 23 | 24 | Global.handler["/"] = index 25 | Global.handler["/login"] = Login 26 | Global.handler["/random_key"] = randomKey 27 | Global.handler["/send_sms"] = sendSMSCN 28 | Global.handler["/history"] = historyCN 29 | Global.handler["/send_sms_cn"] = sendSMSCN 30 | Global.handler["/history_cn"] = historyCN 31 | Global.handler["/send_sms_us"] = sendSMSUS 32 | Global.handler["/history_us"] = historyUS 33 | Global.handler["/help"] = help 34 | } 35 | -------------------------------------------------------------------------------- /model/msg.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/Akvicor/util" 6 | ) 7 | 8 | const ( 9 | _ int = iota 10 | MsgTagSmsReceived 11 | MsgTagSmsSend 12 | MsgTagSmsACK 13 | ) 14 | 15 | type MSG struct { 16 | Tag int `json:"tag"` 17 | Md5 string `json:"md5"` 18 | Data string `json:"data"` 19 | SMS *SMS `json:"-"` 20 | } 21 | 22 | func NewMSG(tag int, sms []*SMS) []*MSG { 23 | msg := make([]*MSG, 0, len(sms)) 24 | for _, v := range sms { 25 | m := &MSG{ 26 | Tag: tag, 27 | Md5: "", 28 | Data: v.String(), 29 | SMS: v, 30 | } 31 | m.GenerateMd5() 32 | msg = append(msg, m) 33 | } 34 | return msg 35 | } 36 | 37 | func UnmarshalMSG(data []byte) *MSG { 38 | msg := &MSG{} 39 | err := json.Unmarshal(data, msg) 40 | if err != nil { 41 | return nil 42 | } 43 | return msg 44 | } 45 | 46 | func (m *MSG) GenerateMd5() { 47 | m.Md5 = util.NewMD5().FromString(m.Data).Upper() 48 | } 49 | 50 | func (m *MSG) Bytes() []byte { 51 | data, err := json.Marshal(m) 52 | if err != nil { 53 | return nil 54 | } 55 | return data 56 | } 57 | 58 | func (m *MSG) String() string { 59 | data, err := json.Marshal(m) 60 | if err != nil { 61 | return "" 62 | } 63 | return string(data) 64 | } 65 | -------------------------------------------------------------------------------- /app/session.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/Akvicor/glog" 5 | "net/http" 6 | "sms/config" 7 | ) 8 | 9 | func SessionVerify(r *http.Request) bool { 10 | ses, err := Global.session.Get(r, config.Global.Session.Name) 11 | if err != nil { 12 | return false 13 | } 14 | username, ok := ses.Values["username"].(string) 15 | if !ok { 16 | return false 17 | } 18 | return username == config.Global.Security.Username 19 | } 20 | 21 | func sessionUpdate(w http.ResponseWriter, r *http.Request, user string) { 22 | ses, _ := Global.session.Get(r, config.Global.Session.Name) 23 | if user == config.Global.Security.Username { 24 | ses.Values["username"] = config.Global.Security.Username 25 | ses.Values["password"] = config.Global.Security.Password 26 | } 27 | ses.Options.MaxAge = config.Global.Session.MaxAge 28 | err := ses.Save(r, w) 29 | if err != nil { 30 | glog.Warning("failed to update session") 31 | return 32 | } 33 | } 34 | 35 | func sessionDelete(w http.ResponseWriter, r *http.Request) { 36 | ses, _ := Global.session.Get(r, config.Global.Session.Name) 37 | ses.Values["username"] = "" 38 | ses.Values["password"] = "" 39 | ses.Options.MaxAge = 1 40 | delete(ses.Values, "username") 41 | delete(ses.Values, "password") 42 | err := ses.Save(r, w) 43 | if err != nil { 44 | glog.Warning("failed to delete session") 45 | return 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/model.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Model struct { 4 | BrandName string `ini:"brand_name"` 5 | Prod bool `ini:"prod"` 6 | SerialCN SerialModel `ini:"serial-cn"` 7 | SerialUS SerialModel `ini:"serial-us"` 8 | Server ServerModel `ini:"server"` 9 | Session SessionModel `ini:"session"` 10 | Database DatabaseModel `ini:"database"` 11 | Log LogModel `ini:"log"` 12 | Security SecurityModel `ini:"security"` 13 | } 14 | 15 | type SerialModel struct { 16 | Name string `ini:"name"` 17 | Baud int `ini:"baud"` 18 | SendQueueSize int `ini:"send_queue_size"` 19 | HeartbeatSendInterval uint `ini:"heartbeat_send_interval"` 20 | HeartbeatReceiveTimeout uint `ini:"heartbeat_receive_timeout"` 21 | } 22 | 23 | type ServerModel struct { 24 | HTTPAddr string `ini:"http_addr"` 25 | HTTPPort int `ini:"http_port"` 26 | EnableHTTPS bool `ini:"enable_https"` 27 | SSLCert string `ini:"ssl_cert"` 28 | SSLKey string `ini:"ssl_key"` 29 | } 30 | 31 | type SessionModel struct { 32 | Domain string `ini:"domain"` 33 | Path string `ini:"path"` 34 | Name string `ini:"name"` 35 | MaxAge int `ini:"max_age"` 36 | } 37 | 38 | type DatabaseModel struct { 39 | Path string `ini:"path"` 40 | } 41 | 42 | type LogModel struct { 43 | LogToFile bool `ini:"log_to_file"` 44 | FilePath string `ini:"file_path"` 45 | } 46 | 47 | type SecurityModel struct { 48 | Username string `ini:"username"` 49 | Password string `ini:"password"` 50 | AccessKey string `ini:"access_key"` 51 | } 52 | -------------------------------------------------------------------------------- /model/sms.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type SMS struct { 10 | Phone string `json:"phone"` 11 | Message string `json:"msg"` 12 | Time string `json:"time"` 13 | } 14 | 15 | func NewSMSLong(phone, msg string) []*SMS { 16 | if phone[0] != '+' { 17 | phone = "+86" + phone 18 | } 19 | var smsLen = 0 20 | smsArray := make([]string, 0, 2) 21 | buf := strings.Builder{} 22 | buf.Grow(150) 23 | lenStep := 1 24 | for _, v := range msg { 25 | if uint32(v) >= 128 { 26 | lenStep = 2 27 | break 28 | } 29 | } 30 | for _, v := range msg { 31 | if smsLen == 0 && (v == '\n' || v == ' ') { 32 | continue 33 | } 34 | smsLen += lenStep 35 | if smsLen < 140 { 36 | buf.WriteRune(v) 37 | continue 38 | } else if smsLen == 140 { 39 | buf.WriteRune(v) 40 | smsArray = append(smsArray, buf.String()) 41 | buf.Reset() 42 | smsLen = 0 43 | continue 44 | } else { 45 | smsArray = append(smsArray, buf.String()) 46 | buf.Reset() 47 | smsLen = 0 48 | buf.WriteRune(v) 49 | continue 50 | } 51 | } 52 | if buf.Len() > 0 { 53 | smsArray = append(smsArray, buf.String()) 54 | } 55 | 56 | sms := make([]*SMS, 0, len(smsArray)) 57 | t := time.Now().Format("2006-01-02 15:04:05") 58 | for _, v := range smsArray { 59 | sms = append(sms, &SMS{ 60 | Phone: phone, 61 | Message: v, 62 | Time: t, 63 | }) 64 | } 65 | return sms 66 | } 67 | 68 | func UnmarshalSMS(data []byte) *SMS { 69 | sms := &SMS{} 70 | err := json.Unmarshal(data, sms) 71 | if err != nil { 72 | return nil 73 | } 74 | return sms 75 | } 76 | 77 | func (s *SMS) Bytes() []byte { 78 | data, err := json.Marshal(s) 79 | if err != nil { 80 | return nil 81 | } 82 | return data 83 | } 84 | 85 | func (s *SMS) String() string { 86 | data, err := json.Marshal(s) 87 | if err != nil { 88 | return "" 89 | } 90 | return string(data) 91 | } 92 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/Akvicor/glog" 7 | "github.com/Akvicor/protocol" 8 | "github.com/Akvicor/util" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "sms/app" 13 | "sms/config" 14 | "sms/db" 15 | "sms/serial" 16 | "syscall" 17 | "time" 18 | ) 19 | 20 | func main() { 21 | var err error 22 | 23 | isInit := flag.Bool("i", false, "init database") 24 | c := flag.String("c", "config.ini", "path to config file") 25 | flag.Parse() 26 | 27 | if util.FileStat(*c).NotFile() { 28 | glog.Fatal("missing config [%s]!", *c) 29 | } 30 | 31 | config.Load(*c) 32 | setGlog() 33 | 34 | if *isInit { 35 | initDatabase() 36 | } 37 | if util.FileStat(config.Global.Database.Path).NotFile() { 38 | glog.Fatal("missing database [%s]!", config.Global.Database.Path) 39 | } 40 | 41 | EnableShutDownListener() 42 | serial.EnableSerialCN() 43 | serial.EnableSerialUS() 44 | initApp() 45 | 46 | addr := fmt.Sprintf("%s:%d", config.Global.Server.HTTPAddr, config.Global.Server.HTTPPort) 47 | if config.Global.Server.EnableHTTPS { 48 | glog.Info("ListenAndServe: https://%s", addr) 49 | err = http.ListenAndServeTLS(addr, config.Global.Server.SSLCert, config.Global.Server.SSLKey, app.Global) 50 | } else { 51 | glog.Info("ListenAndServe: http://%s", addr) 52 | err = http.ListenAndServe(addr, app.Global) 53 | } 54 | if err != nil { 55 | glog.Fatal("failed to listen and serve [%s]", err.Error()) 56 | } 57 | 58 | } 59 | 60 | func initApp() { 61 | app.Generate() 62 | } 63 | 64 | func initDatabase() { 65 | db.CreateDatabase() 66 | 67 | os.Exit(0) 68 | } 69 | 70 | func setGlog() { 71 | if config.Global.Log.LogToFile { 72 | err := glog.SetLogFile(config.Global.Log.FilePath) 73 | if err != nil { 74 | glog.Fatal("failed to set log file [%s]", err.Error()) 75 | } 76 | } 77 | if config.Global.Prod { 78 | glog.SetFlag(glog.FlagStd) 79 | protocol.SetLogProd(true) 80 | } else { 81 | glog.SetFlag(glog.FlagStd | glog.FlagShortFile | glog.FlagFunc | glog.FlagSuffix) 82 | protocol.SetLogProd(false) 83 | } 84 | } 85 | 86 | func EnableShutDownListener() { 87 | go func() { 88 | down := make(chan os.Signal, 1) 89 | signal.Notify(down, os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 90 | <-down 91 | go func() { 92 | ticker := time.NewTicker(3 * time.Second) 93 | <-ticker.C 94 | glog.Fatal("Ticker Finished") 95 | }() 96 | 97 | glog.Info("close serial") 98 | serial.KillCN() 99 | serial.KillUS() 100 | 101 | glog.Info("close log file") 102 | if config.Global.Log.LogToFile { 103 | glog.CloseFile() 104 | } 105 | glog.Info("log file closed") 106 | 107 | os.Exit(0) 108 | }() 109 | } 110 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Akvicor/glog v0.2.3 h1:DnaBXyrw4D+VWoyDNRHIxEZd7BmLtcbxLCNPf59ZEzE= 2 | github.com/Akvicor/glog v0.2.3/go.mod h1:nsmhUDfoMgmC3SmbzplI5itOPChChq0YtDK6jzZZDg0= 3 | github.com/Akvicor/protocol v0.2.3 h1:SsDzz6chQxXTzWLa+8E9gyUismz7c3fbYFrwxiTjmTs= 4 | github.com/Akvicor/protocol v0.2.3/go.mod h1:6r+QKt2hXYZ6x7/JAN16eFkC7Pz9/1Z85lh0s9Rh/oo= 5 | github.com/Akvicor/util v1.10.7 h1:FTR6ucXVjIiK1Gs6lPeNxZTjWYHsjZ2veq0e68Lhs/Q= 6 | github.com/Akvicor/util v1.10.7/go.mod h1:vL2GIOqnq/vTsUSxpbEVDBkqCt/Yavc5r87IUUA9Brg= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= 9 | github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 10 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 11 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 12 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 13 | github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= 14 | github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= 15 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 16 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 17 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 18 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 19 | github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= 20 | github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 21 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 22 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 25 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 26 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= 27 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 28 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 29 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= 32 | gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= 33 | gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A= 34 | gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 35 | -------------------------------------------------------------------------------- /db/history.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/Akvicor/glog" 5 | "html" 6 | "sms/model" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | var historyLock = sync.RWMutex{} 12 | 13 | type HistoryModel struct { 14 | ID int64 `gorm:"column:id;primaryKey;autoIncrement"` 15 | Country string `gorm:"column:country"` 16 | Sender string `gorm:"column:sender"` 17 | RecordTime int64 `gorm:"column:record_time"` 18 | Phone string `gorm:"column:phone"` 19 | Message string `gorm:"column:message"` 20 | Time int64 `gorm:"column:time"` 21 | SentTime int64 `gorm:"column:sent_time"` 22 | } 23 | 24 | func (HistoryModel) TableName() string { 25 | return "history" 26 | } 27 | 28 | func (h *HistoryModel) Format() HistoryFormatModel { 29 | his := HistoryFormatModel{ 30 | ID: h.ID, 31 | Sender: html.EscapeString(h.Sender), 32 | Country: html.EscapeString(h.Country), 33 | RecordTime: "", 34 | Phone: html.EscapeString(h.Phone), 35 | Message: html.EscapeString(h.Message), 36 | Time: "", 37 | SentTime: "", 38 | } 39 | if h.RecordTime != 0 { 40 | his.RecordTime = html.EscapeString(time.Unix(h.RecordTime, 0).Format("2006-01-02 15:04:05")) 41 | } 42 | if h.Time != 0 { 43 | his.Time = html.EscapeString(time.Unix(h.Time, 0).Format("2006-01-02 15:04:05")) 44 | } 45 | if h.SentTime != 0 { 46 | his.SentTime = html.EscapeString(time.Unix(h.SentTime, 0).Format("2006-01-02 15:04:05")) 47 | } 48 | return his 49 | } 50 | 51 | type HistoryFormatModel struct { 52 | ID int64 53 | Country string 54 | Sender string 55 | RecordTime string 56 | Phone string 57 | Message string 58 | Time string 59 | SentTime string 60 | } 61 | 62 | func GetAllHistories(country string, desc bool) []HistoryModel { 63 | d := Connect() 64 | if d == nil { 65 | return nil 66 | } 67 | d = d.Model(&HistoryModel{}).Where("country = ?", country) 68 | historyLock.RLock() 69 | defer historyLock.RUnlock() 70 | 71 | histories := make([]HistoryModel, 0) 72 | if desc { 73 | d = d.Order("id DESC").Find(&histories) 74 | } else { 75 | d = d.Find(&histories) 76 | } 77 | if d.Error != nil { 78 | glog.Warning("get all histories failed [%v] [%v]", d.Error, d.RowsAffected) 79 | return nil 80 | } 81 | return histories 82 | } 83 | 84 | func InsertHistory(country string, sender string, sms *model.SMS) int64 { 85 | if sms == nil { 86 | return 0 87 | } 88 | d := Connect() 89 | if d == nil { 90 | return -1 91 | } 92 | d = d.Model(&HistoryModel{}) 93 | historyLock.RLock() 94 | defer historyLock.RUnlock() 95 | 96 | tu := int64(0) 97 | t, err := time.ParseInLocation("2006-01-02 15:04:05", sms.Time, time.Local) 98 | if err == nil { 99 | tu = t.Unix() 100 | } 101 | now := time.Now().Unix() 102 | his := &HistoryModel{ 103 | Country: country, 104 | Sender: sender, 105 | RecordTime: now, 106 | Phone: sms.Phone, 107 | Message: sms.Message, 108 | Time: tu, 109 | SentTime: 0, 110 | } 111 | res := d.Create(his) 112 | if res.Error != nil || res.RowsAffected != 1 { 113 | glog.Warning("insert history failed [%v] [%v]", res.Error, res.RowsAffected) 114 | return -1 115 | } 116 | return his.ID 117 | } 118 | 119 | func UpdateHistorySent(id int64) bool { 120 | d := Connect() 121 | if d == nil { 122 | return false 123 | } 124 | d = d.Model(&HistoryModel{}) 125 | historyLock.RLock() 126 | defer historyLock.RUnlock() 127 | 128 | res := d.Where("id = ?", id).Update("sent_time", time.Now().Unix()) 129 | 130 | if res.Error != nil || res.RowsAffected != 1 { 131 | glog.Warning("update [%d] history sent failed [%v] [%v]", id, res.Error, res.RowsAffected) 132 | return false 133 | } 134 | return true 135 | } 136 | -------------------------------------------------------------------------------- /serial/serial_us.go: -------------------------------------------------------------------------------- 1 | package serial 2 | 3 | import ( 4 | "github.com/Akvicor/glog" 5 | "github.com/Akvicor/protocol" 6 | "github.com/patrickmn/go-cache" 7 | "github.com/tarm/serial" 8 | "sms/config" 9 | "sms/db" 10 | "sms/model" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | var usSentCache *cache.Cache 16 | var protUS *protocol.Protocol 17 | 18 | const tagUS = "Air780E-US" 19 | 20 | func EnableSerialUS() { 21 | // 防止重复发送 22 | usSentCache = cache.New(3*time.Minute, 5*time.Minute) 23 | // 打开串口 24 | conn, err := serial.OpenPort(&serial.Config{Name: config.Global.SerialUS.Name, Baud: config.Global.SerialUS.Baud}) 25 | if err != nil { 26 | glog.Fatal("failed to open serial %v", err) 27 | } 28 | protUS = protocol.New(tagUS, conn, conn, config.Global.SerialUS.SendQueueSize, 29 | readCallbackUS, heartbeatFailedUS, nil, nil, func() { 30 | time.Sleep(3 * time.Second) 31 | }, nil, func() { 32 | _ = conn.Close() 33 | }) 34 | protUS.SetHeartbeatInterval(uint8(config.Global.SerialUS.HeartbeatSendInterval)) 35 | protUS.SetHeartbeatTimeout(uint8(config.Global.SerialUS.HeartbeatReceiveTimeout)) 36 | protUS.Connect(true) 37 | } 38 | 39 | func heartbeatFailedUS(p *protocol.Protocol) bool { 40 | glog.Trace("[%s] heartbeat failed", p.GetTag()) 41 | return true 42 | } 43 | 44 | func readCallbackUS(data []byte) { 45 | msg := model.UnmarshalMSG(data) 46 | if msg == nil { 47 | glog.Warning("[%s] unmarshal msg failed, rev: %s\n", tagUS, string(data)) 48 | return 49 | } 50 | if msg.Tag == model.MsgTagSmsReceived { 51 | sms := model.UnmarshalSMS([]byte(msg.Data)) 52 | if sms == nil { 53 | glog.Warning("[%s] unmarshal sms failed, data: %s\n", tagUS, msg.Data) 54 | return 55 | } 56 | db.InsertHistory("US", tagUS, sms) 57 | glog.Info("[%s] [Received] sms Phone:[%s] Time:[%s] Message:[%s]", tagUS, sms.Phone, sms.Time, sms.Message) 58 | if sms.Message == "hello" { 59 | if strings.Contains(sms.Phone, selfPhoneCN) { 60 | SendUS("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "Hello Akvicor! here is sms"))) 61 | } else { 62 | SendUS("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "Hello! here is sms"))) 63 | } 64 | } else if sms.Message == "你好" { 65 | if strings.Contains(sms.Phone, selfPhoneCN) { 66 | SendUS("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "你好Akvicor!这里是sms"))) 67 | } else { 68 | SendUS("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "你好!这里是sms"))) 69 | } 70 | } 71 | } else if msg.Tag == model.MsgTagSmsACK { 72 | ack := model.UnmarshalACK([]byte(msg.Data)) 73 | if ack == nil { 74 | glog.Warning("[%s] unmarshal ack failed, data: %s\n", tagUS, msg.Data) 75 | return 76 | } 77 | sendUSMap.Trick(ack.Key) 78 | } 79 | } 80 | 81 | func SendUS(sender string, msg []*model.MSG) { 82 | for _, v := range msg { 83 | go sendUS(sender, v) 84 | } 85 | } 86 | 87 | var sendUSMap = NewSyncMap() 88 | 89 | func sendUS(sender string, msg *model.MSG) { 90 | _, ok := usSentCache.Get(msg.SMS.Phone + msg.SMS.Message) 91 | if ok { 92 | msg.SMS.Time = "D:" + msg.SMS.Time 93 | msg.GenerateMd5() 94 | } else { 95 | usSentCache.Set(msg.SMS.Phone+msg.SMS.Message, struct{}{}, 5*time.Minute) 96 | } 97 | id := db.InsertHistory("US", sender, msg.SMS) 98 | if !ok { 99 | c := sendUSMap.Put(msg.Md5) 100 | send := func() { 101 | err := protUS.Write(msg.Bytes()) 102 | for err != nil { 103 | time.Sleep(3 * time.Second) 104 | err = protUS.Write(msg.Bytes()) 105 | } 106 | } 107 | send() 108 | func() { 109 | retry := 0 110 | for { 111 | select { 112 | case <-time.After(30 * time.Second): 113 | send() 114 | case <-c: 115 | return 116 | } 117 | if retry >= 10 { 118 | return 119 | } 120 | retry += 1 121 | } 122 | }() 123 | sendUSMap.Delete(msg.Md5) 124 | } 125 | db.UpdateHistorySent(id) 126 | glog.Trace("[%s] [Send] send Sender:[%s] Message:[%s]", tagUS, sender, msg.String()) 127 | } 128 | 129 | func KillUS() { 130 | if protUS != nil { 131 | protUS.Kill() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /serial/serial_cn.go: -------------------------------------------------------------------------------- 1 | package serial 2 | 3 | import ( 4 | "github.com/Akvicor/glog" 5 | "github.com/Akvicor/protocol" 6 | "github.com/Akvicor/util" 7 | "github.com/patrickmn/go-cache" 8 | "github.com/tarm/serial" 9 | "sms/config" 10 | "sms/db" 11 | "sms/model" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var cnSentCache *cache.Cache 17 | var protCN *protocol.Protocol 18 | 19 | const tagCN = "Air780E-CN" 20 | const selfPhoneCN = "12345678900" 21 | 22 | func EnableSerialCN() { 23 | // 防止重复发送 24 | cnSentCache = cache.New(3*time.Minute, 5*time.Minute) 25 | // 打开串口 26 | conn, err := serial.OpenPort(&serial.Config{Name: config.Global.SerialCN.Name, Baud: config.Global.SerialCN.Baud}) 27 | if err != nil { 28 | glog.Fatal("failed to open serial %v", err) 29 | } 30 | protCN = protocol.New(tagCN, conn, conn, config.Global.SerialCN.SendQueueSize, 31 | readCallbackCN, heartbeatFailedCN, nil, nil, func() { 32 | time.Sleep(3 * time.Second) 33 | }, nil, func() { 34 | _ = conn.Close() 35 | }) 36 | protCN.SetHeartbeatInterval(uint8(config.Global.SerialCN.HeartbeatSendInterval)) 37 | protCN.SetHeartbeatTimeout(uint8(config.Global.SerialCN.HeartbeatReceiveTimeout)) 38 | protCN.Connect(true) 39 | } 40 | 41 | func heartbeatFailedCN(p *protocol.Protocol) bool { 42 | glog.Trace("[%s] heartbeat failed", p.GetTag()) 43 | return true 44 | } 45 | 46 | func readCallbackCN(data []byte) { 47 | msg := model.UnmarshalMSG(data) 48 | if msg == nil { 49 | glog.Warning("[%s] unmarshal msg failed, rev: %s\n", tagCN, string(data)) 50 | return 51 | } 52 | if msg.Tag == model.MsgTagSmsReceived { 53 | sms := model.UnmarshalSMS([]byte(msg.Data)) 54 | if sms == nil { 55 | glog.Warning("[%s] unmarshal sms failed, data: %s\n", tagCN, msg.Data) 56 | return 57 | } 58 | db.InsertHistory("CN", tagCN, sms) 59 | glog.Info("[%s] [Received] sms Phone:[%s] Time:[%s] Message:[%s]", tagCN, sms.Phone, sms.Time, sms.Message) 60 | if sms.Message == "hello" { 61 | if strings.Contains(sms.Phone, selfPhoneCN) { 62 | SendCN("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "Hello Akvicor! here is sms"))) 63 | } else { 64 | SendCN("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "Hello! here is sms"))) 65 | } 66 | } else if sms.Message == "你好" { 67 | if strings.Contains(sms.Phone, selfPhoneCN) { 68 | SendCN("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "你好Akvicor!这里是sms"))) 69 | } else { 70 | SendCN("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "你好!这里是sms"))) 71 | } 72 | } else if sms.Message == "ha.help" && strings.Contains(sms.Phone, selfPhoneCN) { 73 | SendCN("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "[HA][HELP]\nha.op.reboot - Reboot OP"))) 74 | } else if sms.Message == "ha.op.reboot" && strings.Contains(sms.Phone, selfPhoneCN) { 75 | _, _ = util.HttpPost("http://127.0.0.1/api/services/script/reboot_router", nil, util.HTTPContentTypeJson, map[string]string{"Authorization": "Bearer xxxxxxx"}) 76 | SendCN("sms", model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(sms.Phone, "Reboot OP"))) 77 | } 78 | } else if msg.Tag == model.MsgTagSmsACK { 79 | ack := model.UnmarshalACK([]byte(msg.Data)) 80 | if ack == nil { 81 | glog.Warning("[%s] unmarshal ack failed, data: %s\n", tagCN, msg.Data) 82 | return 83 | } 84 | sendCNMap.Trick(ack.Key) 85 | } 86 | } 87 | 88 | func SendCN(sender string, msg []*model.MSG) { 89 | for _, v := range msg { 90 | go sendCN(sender, v) 91 | } 92 | } 93 | 94 | var sendCNMap = NewSyncMap() 95 | 96 | func sendCN(sender string, msg *model.MSG) { 97 | _, ok := cnSentCache.Get(msg.SMS.Phone + msg.SMS.Message) 98 | if ok { 99 | msg.SMS.Time = "D:" + msg.SMS.Time 100 | msg.GenerateMd5() 101 | } else { 102 | cnSentCache.Set(msg.SMS.Phone+msg.SMS.Message, struct{}{}, 5*time.Minute) 103 | } 104 | id := db.InsertHistory("CN", sender, msg.SMS) 105 | if !ok { 106 | c := sendCNMap.Put(msg.Md5) 107 | send := func() { 108 | err := protCN.Write(msg.Bytes()) 109 | for err != nil { 110 | time.Sleep(3 * time.Second) 111 | err = protCN.Write(msg.Bytes()) 112 | } 113 | } 114 | send() 115 | func() { 116 | retry := 0 117 | for { 118 | select { 119 | case <-time.After(30 * time.Second): 120 | send() 121 | case <-c: 122 | return 123 | } 124 | if retry >= 2 { 125 | return 126 | } 127 | retry += 1 128 | } 129 | }() 130 | sendCNMap.Delete(msg.Md5) 131 | } 132 | db.UpdateHistorySent(id) 133 | glog.Trace("[%s] [Send] send Sender:[%s] Message:[%s]", tagCN, sender, msg.String()) 134 | } 135 | 136 | func KillCN() { 137 | protCN.Kill() 138 | } 139 | -------------------------------------------------------------------------------- /static/gohtml/header.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "header" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ .title }} 10 | 11 | 154 | 155 | 156 | {{ end }} -------------------------------------------------------------------------------- /app/handler.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/Akvicor/glog" 5 | "github.com/Akvicor/util" 6 | "net/http" 7 | "sms/config" 8 | "sms/db" 9 | "sms/model" 10 | "sms/serial" 11 | "sms/static" 12 | "strconv" 13 | ) 14 | 15 | func staticFavicon(w http.ResponseWriter, r *http.Request) { 16 | _, _ = w.Write(static.Favicon) 17 | } 18 | 19 | func index(w http.ResponseWriter, r *http.Request) { 20 | head, tail := util.SplitPathRepeat(r.URL.Path, 1) 21 | glog.Debug("[%-4s][%-32s] [%s][%s]", r.Method, "/index", head, tail) 22 | if !SessionVerify(r) { 23 | Login(w, r) 24 | return 25 | } 26 | 27 | if r.Method == http.MethodGet { 28 | _ = static.Index.Execute(w, map[string]interface{}{"title": "SMS Pusher"}) 29 | } 30 | } 31 | 32 | func Login(w http.ResponseWriter, r *http.Request) { 33 | head, tail := util.SplitPathRepeat(r.URL.Path, 1) 34 | glog.Debug("[%-4s][%-32s] [%s][%s]", r.Method, "/login", head, tail) 35 | 36 | if r.Method == http.MethodGet { 37 | _ = static.Login.Execute(w, map[string]interface{}{"title": "Login"}) 38 | return 39 | } else if r.Method == http.MethodPost { 40 | username := r.FormValue("username") 41 | password := r.FormValue("password") 42 | if username == config.Global.Security.Username && password == config.Global.Security.Password { 43 | glog.Info("Login successful [%s]", username) 44 | sessionUpdate(w, r, config.Global.Security.Username) 45 | util.RespRedirect(w, r, r.URL.String()) 46 | } else { 47 | glog.Info("Login failed: [%s][%s]", username, password) 48 | util.RespRedirect(w, r, r.URL.String()) 49 | } 50 | } 51 | } 52 | 53 | func randomKey(w http.ResponseWriter, r *http.Request) { 54 | head, tail := util.SplitPathRepeat(r.URL.Path, 1) 55 | glog.Debug("[%-4s][%-32s] [%s][%s]", r.Method, "/random_key", head, tail) 56 | if !SessionVerify(r) { 57 | Login(w, r) 58 | return 59 | } 60 | rRange := r.FormValue("range") 61 | if rRange == "" { 62 | rRange = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" 63 | } 64 | rLength := r.FormValue("length") 65 | cLength := 8 66 | if rLength != "" { 67 | clen, err := strconv.Atoi(rLength) 68 | if err == nil { 69 | cLength = clen 70 | } 71 | } 72 | _, _ = w.Write([]byte(util.RandomString(cLength, rRange))) 73 | return 74 | 75 | } 76 | 77 | func sendSMSCN(w http.ResponseWriter, r *http.Request) { 78 | head, tail := util.SplitPathRepeat(r.URL.Path, 1) 79 | glog.Debug("[%-4s][%-32s] [%s][%s]", r.Method, "/send_sms_cn", head, tail) 80 | 81 | key := "" 82 | phone := "" 83 | message := "" 84 | sender := "" 85 | if r.Method == "GET" { 86 | values := r.URL.Query() 87 | key = values.Get("key") 88 | phone = values.Get("phone") 89 | message = values.Get("message") 90 | sender = values.Get("sender") 91 | } else if r.Method == "POST" { 92 | key = r.PostFormValue("key") 93 | phone = r.PostFormValue("phone") 94 | message = r.PostFormValue("message") 95 | sender = r.PostFormValue("sender") 96 | } 97 | 98 | if r.Method == "GET" && key == "" { 99 | if !SessionVerify(r) { 100 | Login(w, r) 101 | return 102 | } 103 | _ = static.SendSMS.Execute(w, map[string]any{"title": "Send SMS", "url": "/send_sms_cn"}) 104 | return 105 | } 106 | 107 | if (key != config.Global.Security.AccessKey) && (!SessionVerify(r)) { 108 | util.WriteHTTPRespAPIInvalidInput(w, "invalid access key") 109 | return 110 | } 111 | if len(phone) < 1 { 112 | util.WriteHTTPRespAPIInvalidInput(w, "invalid phone number") 113 | return 114 | } 115 | if len(message) < 1 { 116 | util.WriteHTTPRespAPIInvalidInput(w, "invalid message") 117 | return 118 | } 119 | if len(sender) < 1 { 120 | util.WriteHTTPRespAPIInvalidInput(w, "invalid sender") 121 | return 122 | } 123 | 124 | serial.SendCN(sender, model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(phone, message))) 125 | 126 | util.WriteHTTPRespAPIOk(w, nil) 127 | } 128 | 129 | func historyCN(w http.ResponseWriter, r *http.Request) { 130 | head, tail := util.SplitPathRepeat(r.URL.Path, 1) 131 | glog.Debug("[%-4s][%-32s] [%s][%s]", r.Method, "/history_cn", head, tail) 132 | if !SessionVerify(r) { 133 | Login(w, r) 134 | return 135 | } 136 | 137 | if r.Method == http.MethodGet { 138 | his := db.GetAllHistories("CN", true) 139 | glog.Debug("%#v", his) 140 | histories := make([]db.HistoryFormatModel, len(his)) 141 | for k, v := range his { 142 | histories[k] = v.Format() 143 | } 144 | _ = static.History.Execute(w, map[string]any{"title": "History", "histories": histories}) 145 | return 146 | } 147 | } 148 | 149 | func sendSMSUS(w http.ResponseWriter, r *http.Request) { 150 | head, tail := util.SplitPathRepeat(r.URL.Path, 1) 151 | glog.Debug("[%-4s][%-32s] [%s][%s]", r.Method, "/send_sms_us", head, tail) 152 | 153 | key := "" 154 | phone := "" 155 | message := "" 156 | sender := "" 157 | if r.Method == "GET" { 158 | values := r.URL.Query() 159 | key = values.Get("key") 160 | phone = values.Get("phone") 161 | message = values.Get("message") 162 | sender = values.Get("sender") 163 | } else if r.Method == "POST" { 164 | key = r.PostFormValue("key") 165 | phone = r.PostFormValue("phone") 166 | message = r.PostFormValue("message") 167 | sender = r.PostFormValue("sender") 168 | } 169 | 170 | if r.Method == "GET" && key == "" { 171 | if !SessionVerify(r) { 172 | Login(w, r) 173 | return 174 | } 175 | _ = static.SendSMS.Execute(w, map[string]any{"title": "Send SMS", "url": "/send_sms_us"}) 176 | return 177 | } 178 | 179 | if (key != config.Global.Security.AccessKey) && (!SessionVerify(r)) { 180 | util.WriteHTTPRespAPIInvalidInput(w, "invalid access key") 181 | return 182 | } 183 | if len(phone) < 1 { 184 | util.WriteHTTPRespAPIInvalidInput(w, "invalid phone number") 185 | return 186 | } 187 | if len(message) < 1 { 188 | util.WriteHTTPRespAPIInvalidInput(w, "invalid message") 189 | return 190 | } 191 | if len(sender) < 1 { 192 | util.WriteHTTPRespAPIInvalidInput(w, "invalid sender") 193 | return 194 | } 195 | 196 | serial.SendUS(sender, model.NewMSG(model.MsgTagSmsSend, model.NewSMSLong(phone, message))) 197 | 198 | util.WriteHTTPRespAPIOk(w, nil) 199 | } 200 | 201 | func historyUS(w http.ResponseWriter, r *http.Request) { 202 | head, tail := util.SplitPathRepeat(r.URL.Path, 1) 203 | glog.Debug("[%-4s][%-32s] [%s][%s]", r.Method, "/history_us", head, tail) 204 | if !SessionVerify(r) { 205 | Login(w, r) 206 | return 207 | } 208 | 209 | if r.Method == http.MethodGet { 210 | his := db.GetAllHistories("US", true) 211 | glog.Debug("%#v", his) 212 | histories := make([]db.HistoryFormatModel, len(his)) 213 | for k, v := range his { 214 | histories[k] = v.Format() 215 | } 216 | _ = static.History.Execute(w, map[string]any{"title": "History", "histories": histories}) 217 | return 218 | } 219 | } 220 | 221 | func help(w http.ResponseWriter, r *http.Request) { 222 | head, tail := util.SplitPathRepeat(r.URL.Path, 1) 223 | glog.Debug("[%-4s][%-32s] [%s][%s]", r.Method, "/help", head, tail) 224 | if !SessionVerify(r) { 225 | Login(w, r) 226 | return 227 | } 228 | 229 | if r.Method == http.MethodGet { 230 | w.Write([]byte(` 231 | GET: 232 | /random_key?range=(不提供则使用默认值)&length=(默认为8) 233 | POST: 234 | /send_sms?key=(访问密钥,如果已通过网页登录则不需要)&sender=(发送者)&phone=(手机号)&message=(短信内容) 235 | `)) 236 | return 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /air780e/main.lua: -------------------------------------------------------------------------------- 1 | PROJECT = "sms" 2 | VERSION = "0.0.2" 3 | 4 | log.info("main", PROJECT, VERSION) 5 | 6 | sys = require("sys") 7 | require "sysplus" -- http库需要这个sysplus 8 | 9 | if wdt then 10 | --添加硬狗防止程序卡死,在支持的设备上启用这个功能 11 | wdt.init(9000)--初始化watchdog设置为9s 12 | sys.timerLoopStart(wdt.feed, 3000)--3s喂一次狗 13 | end 14 | 15 | ---------------------------------------------------------------- 16 | -- 通用消息处理 17 | -- 18 | -- 与上位机数据通信格式,转化为base64格式发送 19 | -- {tag:int, md5:string, data=string} 20 | -- tag: int类型数据,表示不同的数据格式 21 | -- md5: string类型数据,表示数据的md5校验值 22 | -- data: string类型数据,数据base64化后的字符串 23 | 24 | PROTOCOL_VERSION = 1 25 | FLAG_HEARTBEAT = 1 26 | FLAG_HEARTBEAT_REQUEST = 2 27 | ENCRYPT_NONE = 0 28 | 29 | TAG_SMS_RECEIVED = 1 30 | TAG_SMS_SEND = 2 31 | TAG_SMS_ACK = 3 32 | 33 | -- 处理接收到的通用消息 34 | -- data:string 通用消息 35 | function msg_handler(data) 36 | local msg = json.decode(data) -- 解析json 37 | if msg == nil then 38 | log.info(" body == nil", data) 39 | return 40 | end 41 | -- 校验数据md5 42 | local md5 = crypto.md5(msg.data) 43 | if md5 ~= msg.md5 then 44 | log.info(" md5 ~= msg.md5", md5, msg.md5) 45 | return 46 | end 47 | -- 消息处理 48 | if msg.tag == TAG_SMS_SEND then 49 | -- 解析sms 50 | local vsms = json.decode(msg.data) 51 | if vsms == nil then 52 | log.info(" sms == nil", msg.data) 53 | return 54 | end 55 | log.info("send sms: ", vsms.phone, vsms.msg) 56 | local res = sms.send(vsms.phone, vsms.msg) 57 | if res then 58 | msg_send(TAG_SMS_ACK, json.encode({key=md5})) 59 | end 60 | return 61 | end 62 | log.info("sms_handler", data) 63 | end 64 | 65 | -- 发送通用消息 66 | -- tag:int 消息类型 67 | -- data:string 消息数据 68 | 69 | function msg_send(tag, data) 70 | --log.info("msg_send", data) 71 | local pkg = string.char(0xff, 0x07, 0x55, 0x00) 72 | pkg = pkg .. string.char(PROTOCOL_VERSION) 73 | local msg = json.encode({tag=tag, md5=crypto.md5(data), data=data}) 74 | local headFooter = string.char(0, ENCRYPT_NONE, 0) .. Int32ToBuf(#msg) .. Int32ToBuf(crypto.crc32(msg)) 75 | pkg = pkg .. Int32ToBuf(crypto.crc32(headFooter)) .. headFooter .. msg 76 | --log.info("msg_send", #pkg, pkg:toHex()) 77 | uart.write(UART_ID, pkg) 78 | end 79 | 80 | function heartbeat_response() 81 | log.info("[heartbeat_send] response send") 82 | local pkg = string.char(0xff, 0x07, 0x55, 0x00) 83 | pkg = pkg .. string.char(PROTOCOL_VERSION) 84 | local headFooter = string.char(FLAG_HEARTBEAT, ENCRYPT_NONE, 0) .. Int32ToBuf(0) .. Int32ToBuf(crypto.crc32("")) 85 | pkg = pkg .. Int32ToBuf(crypto.crc32(headFooter)) .. headFooter 86 | --log.info("heartbeat_send", #pkg, pkg:toHex()) 87 | uart.write(UART_ID, pkg) 88 | end 89 | 90 | ---------------------------------------------------------------- 91 | -- UART 92 | 93 | UART_ID = 1 94 | 95 | -- 初始化UART 96 | local uart_ok = uart.setup( 97 | UART_ID, --串口id 98 | 115200, --波特率 99 | 8, --数据位 100 | 1, --停止位 101 | uart.NONE, --校验位 102 | uart.LSB, --大小端 103 | 4112 --缓冲区大小 104 | ) 105 | 106 | PACKAGE_MAX_SIZE = 4096 107 | PACKAGE_HEAD_SIZE = 20 108 | 109 | HEAD_OFFSET_PREFIX = 0 + 1 110 | HEAD_OFFSET_VERSION = 4 + 1 111 | HEAD_OFFSET_CRC32 = 5 + 1 112 | HEAD_OFFSET_FLAG = 9 + 1 113 | HEAD_OFFSET_ENCRYPT = 10 + 1 114 | HEAD_OFFSET_VALUE = 11 + 1 115 | HEAD_OFFSET_DATA_SIZE = 12 + 1 116 | HEAD_OFFSET_DATA_CRC32 = 16 + 1 117 | HEAD_OFFSET_DATA = 20 + 1 118 | 119 | ENCRYPT_NONE = 0 120 | 121 | r_buf = "" 122 | 123 | -- 设置UART回调函数 124 | uart.on(UART_ID, "receive", function(id, len) 125 | repeat 126 | local s = uart.read(id, len) 127 | r_buf = s 128 | if #r_buf >= PACKAGE_HEAD_SIZE then 129 | -- prefix 130 | if string.byte(r_buf, HEAD_OFFSET_PREFIX) ~= 0xff then 131 | log.info(" uart: error on 0xff") 132 | break 133 | end 134 | if string.byte(r_buf, HEAD_OFFSET_PREFIX + 1) ~= 0x07 then 135 | log.info(" uart: error on 0x07") 136 | break 137 | end 138 | if string.byte(r_buf, HEAD_OFFSET_PREFIX + 2) ~= 0x55 then 139 | log.info(" uart: error on 0x55") 140 | break 141 | end 142 | if string.byte(r_buf, HEAD_OFFSET_PREFIX + 3) ~= 0x00 then 143 | log.info(" uart: error on 0x00") 144 | break 145 | end 146 | -- version 147 | if string.byte(r_buf, HEAD_OFFSET_VERSION) ~= PROTOCOL_VERSION then 148 | log.info(" uart: error on prot version") 149 | break 150 | end 151 | -- crc32 152 | local crc32 = bufToUInt32(string.sub(r_buf, HEAD_OFFSET_CRC32, HEAD_OFFSET_CRC32 + 3)) 153 | local crc32_got = crypto.crc32(string.sub(r_buf, HEAD_OFFSET_FLAG, HEAD_OFFSET_DATA-1)) 154 | if crc32 ~= crc32_got then 155 | log.info(string.format("%x%x%x%x", string.byte(r_buf, HEAD_OFFSET_CRC32), string.byte(r_buf, HEAD_OFFSET_CRC32+1), string.byte(r_buf, HEAD_OFFSET_CRC32+2), string.byte(r_buf, HEAD_OFFSET_CRC32+3))) 156 | log.info(" uart: error on crc32, need ", string.format("%x", crc32), " got ", string.format("%x", crc32_got)) 157 | break 158 | end 159 | -- flag 160 | if (string.byte(r_buf, HEAD_OFFSET_FLAG) & FLAG_HEARTBEAT) ~= 0 then 161 | log.info(" uart: heartbeat received") 162 | heartbeat_received = true 163 | end 164 | if (string.byte(r_buf, HEAD_OFFSET_FLAG) & FLAG_HEARTBEAT_REQUEST) ~= 0 then 165 | log.info(" uart: heartbeat request received") 166 | heartbeat_received = true 167 | heartbeat_response() 168 | end 169 | -- encrypt method 170 | if string.byte(r_buf, HEAD_OFFSET_ENCRYPT) ~= ENCRYPT_NONE then 171 | log.info(" uart: error on encrypt method") 172 | break 173 | end 174 | -- value 175 | -- data size 176 | local data_size = bufToUInt32(string.sub(r_buf, HEAD_OFFSET_DATA_SIZE, HEAD_OFFSET_DATA_SIZE + 3)) 177 | local data_crc32 = bufToUInt32(string.sub(r_buf, HEAD_OFFSET_DATA_CRC32, HEAD_OFFSET_DATA_CRC32 + 3)) 178 | local data = string.sub(r_buf, HEAD_OFFSET_DATA, HEAD_OFFSET_DATA+data_size) 179 | local data_crc32_got = crypto.crc32(data) 180 | if data_crc32 ~= data_crc32_got then 181 | log.info(" uart: error on data crc32") 182 | break 183 | end 184 | msg_handler(data) 185 | end 186 | if #s == len then 187 | break 188 | end 189 | until s == "" 190 | end) 191 | 192 | 193 | ---------------------------------------------------------------- 194 | -- SMS 195 | 196 | -- 接受短信回调函数 197 | -- 1. 通过uart发送到ha,由ha进一步处理 198 | -- num 手机号码 199 | -- txt 文本内容 200 | function sms_handler(num, txt, meta) 201 | if txt == "help" then 202 | sms.send("+8612345678900", "[SMS][HELP]\nreboot - Reboot SMS\nstatus - SMS Status\ncstatus - SMS Current Status") 203 | return 204 | end 205 | if txt == "reboot" then 206 | sms.send("+8612345678900", "[SMS][Reboot]") 207 | sys.wait(3000) 208 | pm.reboot() 209 | return 210 | end 211 | if txt == "status" then 212 | send_status() 213 | return 214 | end 215 | if txt == "cstatus" then 216 | send_status() 217 | return 218 | end 219 | local body = json.encode({phone=num, msg=txt, time=string.format("20%02d-%02d-%02d %02d:%02d:%02d", meta.year, meta.mon, meta.day, meta.hour, meta.min, meta.sec)}) -- 短信数据json 220 | msg_send(TAG_SMS_RECEIVED, body) 221 | end 222 | -- 设置短信回调函数 223 | sms.setNewSmsCb(sms_handler) 224 | 225 | ---------------------------------------------------------------- 226 | -- HEARTBEAT 227 | 228 | function send_status() 229 | status="[SMS][HA]" 230 | if power_last_state == 0 then 231 | status=status .. " down" 232 | elseif power_last_state == 1 then 233 | status=status .. " up" 234 | else 235 | status=status .. " nil" 236 | end 237 | if ha_last_state then 238 | status=status .. " connected" 239 | else 240 | status=status .. " disconnected" 241 | end 242 | sms.send("+8612345678900", status) 243 | end 244 | 245 | function send_cstatus() 246 | status="[SMS][HA]" 247 | power_state = gpio.get(GPIO_HA_POWER_PIN) 248 | if power_state == 0 then 249 | status=status .. " down" 250 | elseif power_state == 1 then 251 | status=status .. " up" 252 | else 253 | status=status .. " nil" 254 | end 255 | if heartbeat_received then 256 | status=status .. " connected" 257 | else 258 | status=status .. " disconnected" 259 | end 260 | sms.send("+8612345678900", status) 261 | end 262 | 263 | GPIO_HA_POWER_PIN = 1 264 | gpio.setup(GPIO_HA_POWER_PIN, nil, gpio.PULLUP) 265 | -- power last state 电源上一次的状态 266 | -- 0 power down 267 | -- 1 power up 268 | power_last_state = 1 269 | power_last_state_times = -1 270 | 271 | function check_power() 272 | power_state = gpio.get(GPIO_HA_POWER_PIN) 273 | log.info("[check_connection] GPIO ", string.format("%d", power_state)) 274 | if power_state ~= power_last_state then 275 | if power_last_state_times == 0 then 276 | -- 状态仍是变化后的,等待次数为0,发出提醒 277 | send_healthy_power() 278 | -- 修改上次的状态,更改为当前状态,不再发出此状态的提醒 279 | power_last_state = power_state 280 | power_last_state_times = -1 281 | elseif power_last_state_times == -1 then 282 | -- 状态刚发生变化,初始化次数等待发送信息 283 | power_last_state_times = 3 284 | else 285 | -- 状态仍是变化后的,减少等待次数 286 | power_last_state_times = power_last_state_times - 1 287 | end 288 | else 289 | -- 结果与上次相同,忽略(如果中间掺杂其他状态,忽略 290 | power_last_state_times = -1 291 | end 292 | end 293 | 294 | function send_healthy_power() 295 | if power_state == 0 then 296 | sms.send("+8612345678900", "[SMS][HA] power down") 297 | else 298 | sms.send("+8612345678900", "[SMS][HA] power up") 299 | end 300 | end 301 | 302 | heartbeat_received = false 303 | -- ha last state HA上一次的状态 304 | -- true connected 与ha建立连接 305 | -- false disconnected 与ha断开连接 306 | ha_last_state = true 307 | ha_last_state_times = -1 308 | 309 | function check_connection() 310 | ha_state = false 311 | if heartbeat_received then 312 | log.info("[check_connection] heartbeat received") 313 | heartbeat_received = false 314 | ha_state = true 315 | else 316 | log.info("[check_connection] heartbeat missing") 317 | ha_state = false 318 | end 319 | if ha_state ~= ha_last_state then 320 | if ha_last_state_times == 0 then 321 | -- 状态仍是变化后的,等待次数为0,发出提醒 322 | send_healthy_ha() 323 | -- 修改上次的状态,更改为当前状态,不再发出此状态的提醒 324 | ha_last_state = ha_state 325 | ha_last_state_times = -1 326 | elseif ha_last_state_times == -1 then 327 | -- 状态刚发生变化,初始化次数等待发送信息 328 | power_last_state_times = 3 329 | else 330 | -- 状态仍是变化后的,减少等待次数 331 | power_last_state_times = power_last_state_times - 1 332 | end 333 | else 334 | -- 结果与上次相同,忽略(如果中间掺杂其他状态,忽略 335 | ha_last_state_times = -1 336 | end 337 | end 338 | 339 | function send_healthy_ha() 340 | if ha_last_state then 341 | sms.send("+8612345678900", "[SMS][HA] connected") 342 | else 343 | sms.send("+8612345678900", "[SMS][HA] disconnected") 344 | end 345 | end 346 | 347 | -- 每10s检查电源是否正常 348 | sys.timerLoopStart(check_power, 10000) 349 | -- 每60s检查连接是否正常 350 | sys.timerLoopStart(check_connection, 60000) 351 | -- 每8s发送一次心跳信号 352 | --sys.timerLoopStart(heartbeat_send, 8000) 353 | 354 | ---------------------------------------------------------------- 355 | -- UTIL 356 | 357 | function bufToUInt32(buf) 358 | return (string.byte(buf, 1) << 24) | (string.byte(buf, 2) << 16) | (string.byte(buf, 3) << 8) | (string.byte(buf, 4)) 359 | end 360 | 361 | function Int32ToBuf(n) 362 | return (string.format("%c%c%c%c",n >> 24,n >> 16,n >> 8,n)) 363 | end 364 | 365 | ---------------------------------------------------------------- 366 | -- MAIN 367 | 368 | sys.taskInit(function() 369 | sys.wait(60000) 370 | send_status() 371 | end) 372 | 373 | sys.run() 374 | --------------------------------------------------------------------------------