├── .gitignore ├── bin ├── upload.txt ├── build.cmd └── sourceforge.py ├── mqtt ├── README.md ├── LICENSE └── mqtt.go ├── crypt ├── README.md └── crypt.go ├── serial ├── README.md ├── open_linux.go ├── serial.go ├── open_windows.go └── LICENSE ├── go.mod ├── btapp_reader.go ├── mqtt_reader.go ├── dict └── dict.go ├── gap ├── parsers.go ├── gap.go └── mibeacon.go ├── device_gateway.go ├── go.sum ├── device_ble.go ├── shell.go ├── README.md ├── gw3.go ├── miio_reader.go ├── btchip_reader.go └── bglib └── bglib.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | bin/gw3 3 | 4 | .idea/ 5 | -------------------------------------------------------------------------------- /bin/upload.txt: -------------------------------------------------------------------------------- 1 | quote pasv 2 | binary 3 | put gw3 /data/gw3 4 | disconnect 5 | quit -------------------------------------------------------------------------------- /mqtt/README.md: -------------------------------------------------------------------------------- 1 | # mqtt 2 | 3 | Original source: [github](https://github.com/jeffallen/mqtt) 4 | 5 | Fixed **Last will** message 6 | -------------------------------------------------------------------------------- /crypt/README.md: -------------------------------------------------------------------------------- 1 | # AES CCM Mode 2 | 3 | Original source: [gist](https://gist.github.com/hirochachacha/abb76ff71573dea2ef42) 4 | 5 | Fixed for disable validation with tag len = 1 6 | -------------------------------------------------------------------------------- /serial/README.md: -------------------------------------------------------------------------------- 1 | # go-serial 2 | 3 | Original source: [github](https://github.com/jacobsa/go-serial) 4 | 5 | Fixed for MIPS from [this issue](https://github.com/jacobsa/go-serial/issues/27) 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AlexxIT/gw3 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/huin/gobinarytest v0.0.0-20170803182140-bc6c19e91749 // indirect 7 | github.com/huin/mqtt v0.0.0-20200914141616-61735481eb15 8 | github.com/rs/zerolog v1.25.0 9 | golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 10 | ) 11 | -------------------------------------------------------------------------------- /bin/build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | set GOOS=linux 3 | set GOARCH=mipsle 4 | 5 | for /f "tokens=1-3 delims=." %%a in ("%date%") do set DATE=%%c.%%b.%%a 6 | for /f %%a in ('git rev-parse --short HEAD') do set COMMIT=%%a 7 | 8 | go build -ldflags "-w -s -X main.version=%DATE%_%COMMIT%" -trimpath -o gw3 .. 9 | upx -q gw3 10 | 11 | if "%1"=="" exit /b 12 | 13 | rem Upload binary to gateway if pass gate IP-address as first param 14 | rem /data/busybox tcpsvd -E 0.0.0.0 21 /data/busybox ftpd -w & 15 | ftp -s:upload.txt %1 16 | -------------------------------------------------------------------------------- /bin/sourceforge.py: -------------------------------------------------------------------------------- 1 | """ 2 | Upload gw3 binary to sourceforge.net with username and password from os environments. And prints md5 of binary. 3 | 4 | https://sourceforge.net/p/forge/documentation/Release%20Files%20for%20Download/#scp 5 | """ 6 | import hashlib 7 | import io 8 | import os 9 | import pathlib 10 | 11 | from paramiko import SSHClient, client 12 | from scp import SCPClient 13 | 14 | ssh = SSHClient() 15 | ssh.set_missing_host_key_policy(client.AutoAddPolicy()) 16 | ssh.connect( 17 | 'frs.sourceforge.net', 18 | username=os.environ['SF_USER'], 19 | password=os.environ['SF_PASS'] 20 | ) 21 | 22 | scp = SCPClient(ssh.get_transport()) 23 | 24 | raw = open('gw3', 'rb').read() 25 | hex_ = hashlib.md5(raw).hexdigest() 26 | print(hex_) 27 | 28 | f = io.BytesIO(raw) 29 | f.seek(0) 30 | scp.putfo(f, '/home/frs/project/mgl03/bin/gw3') 31 | f.seek(0) 32 | scp.putfo(f, '/home/frs/project/mgl03/gw3/' + hex_) 33 | scp.close() 34 | -------------------------------------------------------------------------------- /mqtt/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jeff R. Allen. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * The names of the contributors may not be used to 14 | endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /btapp_reader.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/AlexxIT/gw3/bglib" 5 | "github.com/AlexxIT/gw3/serial" 6 | "github.com/rs/zerolog" 7 | "github.com/rs/zerolog/log" 8 | "io" 9 | "time" 10 | ) 11 | 12 | var btapp io.ReadWriteCloser 13 | 14 | // btappInit open serial connection to /dev/ptyp8 (virtual serial interface) 15 | func btappInit() { 16 | var err error 17 | for i := 1; ; i++ { 18 | btapp, err = serial.Open(serial.OpenOptions{ 19 | PortName: "/dev/ptyp8", 20 | BaudRate: 115200, 21 | DataBits: 8, 22 | StopBits: 1, 23 | MinimumReadSize: 1, 24 | }) 25 | if err == nil { 26 | return 27 | } 28 | if i == 4 { 29 | log.Panic().Err(err).Send() 30 | } 31 | 32 | shellFreeTTY() 33 | 34 | // wait after release 35 | time.Sleep(time.Duration(i) * time.Second) 36 | } 37 | } 38 | 39 | func btappReader() { 40 | // only one msg per 10 seconds 41 | sampler := log.Sample(&zerolog.BurstSampler{ 42 | Burst: 1, 43 | Period: 10 * time.Second, 44 | }) 45 | 46 | var p = make([]byte, 1024) 47 | for { 48 | n, err := btapp.Read(p) 49 | if err != nil { 50 | sampler.Debug().Err(err).Msg("btapp.Read") 51 | continue 52 | } 53 | 54 | //log.WithLevel(btraw).Hex("data", p[:n]).Msg("queue<-") 55 | 56 | header := uint32(p[0])<<24 | uint32(p[2])<<8 | uint32(p[3]) 57 | switch header { 58 | case bglib.Cmd_system_reset: 59 | log.Debug().Msg("<-cmd_system_reset") 60 | 61 | btchipQueueClear() 62 | btchipRespClear() 63 | 64 | case bglib.Cmd_le_gap_set_discovery_timing: 65 | //log.Debug().Int("scan_interval", 0x10).Msg("cmd_le_gap_set_discovery_timing") 66 | 67 | //bglib.PatchGapDiscoveryTiming(p, 0x10, 0x10) 68 | 69 | case bglib.Cmd_le_gap_start_discovery: 70 | //log.Info().Uint8("mode", p[5]).Msg("cmd_le_gap_start_discovery") 71 | 72 | // enable extended scan before start cmd 73 | btchipQueueAdd(bglib.EncodeGapExtendedScan(1)) 74 | 75 | case bglib.Cmd_mesh_node_set_ivrecovery_mode: 76 | log.Info().Uint8("enable", p[4]).Msg("<-cmd_mesh_node_set_ivrecovery_mode") 77 | } 78 | 79 | btchipQueueAdd(p[:n]) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /mqtt_reader.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/AlexxIT/gw3/mqtt" 7 | proto "github.com/huin/mqtt" 8 | "github.com/rs/zerolog/log" 9 | "net" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | var mqttClient *mqtt.ClientConn 15 | 16 | func mqttReader() { 17 | for { 18 | conn, err := net.Dial("tcp", "127.0.0.1:1883") 19 | if err != nil { 20 | log.Error().Caller().Err(err).Send() 21 | } else { 22 | mqttClient = mqtt.NewClientConn(conn) 23 | if err = mqttClient.Connect(&proto.Connect{ 24 | ClientId: "gw3", 25 | WillRetain: true, 26 | WillTopic: "gw3/"+gw.WiFi.MAC+"/state", 27 | WillMessage: `{"state":"offline"}`, 28 | }); err != nil { 29 | log.Error().Caller().Err(err).Send() 30 | } else { 31 | gw.updateInfo() 32 | mqttClient.Subscribe([]proto.TopicQos{ 33 | {Topic: "gw3/+/set"}, 34 | }) 35 | for m := range mqttClient.Incoming { 36 | buf := bytes.Buffer{} 37 | if err = m.Payload.WritePayload(&buf); err != nil { 38 | log.Error().Caller().Err(err).Send() 39 | continue 40 | } 41 | 42 | items := strings.Split(m.TopicName, "/") 43 | if len(items) == 3 && items[2] == "set" { 44 | mac := items[1] 45 | if device, ok := devices[mac]; ok { 46 | device.(DeviceGetSet).setState(buf.Bytes()) 47 | } 48 | } 49 | } 50 | } 51 | mqttClient = nil 52 | } 53 | time.Sleep(time.Second) 54 | } 55 | } 56 | 57 | func mqttPublish(topic string, data interface{}, retain bool) { 58 | if mqttClient == nil { 59 | return 60 | } 61 | 62 | var payload []byte 63 | 64 | switch data.(type) { 65 | case []byte: 66 | payload = data.([]byte) 67 | case string: 68 | payload = []byte(data.(string)) 69 | default: 70 | var err error 71 | if payload, err = json.Marshal(data); err != nil { 72 | log.Warn().Err(err).Send() 73 | return 74 | } 75 | } 76 | 77 | //var re = regexp.MustCompile(`([0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}):[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}`) 78 | //topic = re.ReplaceAllString(topic, `$1:FF:FF:FF`) 79 | //payload = re.ReplaceAll(payload, []byte(`$1:FF:FF:FF`)) 80 | 81 | msg := &proto.Publish{ 82 | Header: proto.Header{Retain: retain}, 83 | TopicName: topic, 84 | Payload: proto.BytesPayload(payload), 85 | } 86 | mqttClient.Publish(msg) 87 | } 88 | 89 | type mqttLogWriter struct{} 90 | 91 | func (m mqttLogWriter) Write(p []byte) (n int, err error) { 92 | if mqttClient != nil { 93 | msg := &proto.Publish{ 94 | Header: proto.Header{}, 95 | TopicName: "gw3/stdout", 96 | Payload: proto.BytesPayload(p), 97 | } 98 | mqttClient.Publish(msg) 99 | } 100 | 101 | return len(p), nil 102 | } 103 | -------------------------------------------------------------------------------- /dict/dict.go: -------------------------------------------------------------------------------- 1 | // Example: 2 | // 3 | // payload, err := dict.Unmarshal(b) 4 | // 5 | // if value, ok := payload.TryGetString("value"); ok { 6 | // print(value) 7 | // } 8 | // 9 | // value := payload.GetDict("result").GetString("value", "default") 10 | package dict 11 | 12 | import ( 13 | "encoding/json" 14 | ) 15 | 16 | type Dict map[string]interface{} 17 | 18 | // Decode JSON object to Dict class 19 | func Unmarshal(b []byte) (*Dict, error) { 20 | payload := make(Dict) 21 | if err := json.Unmarshal(b, &payload); err != nil { 22 | return nil, err 23 | } 24 | return &payload, nil 25 | } 26 | 27 | func (d *Dict) TryGetString(name string) (string, bool) { 28 | switch (*d)[name].(type) { 29 | case string: 30 | return (*d)[name].(string), true 31 | } 32 | return "", false 33 | } 34 | 35 | func (d *Dict) TryGetNumber(name string) (float64, bool) { 36 | switch (*d)[name].(type) { 37 | case float64: 38 | return (*d)[name].(float64), true 39 | } 40 | return 0, false 41 | } 42 | 43 | func (d *Dict) GetDict(name string) *Dict { 44 | switch (*d)[name].(type) { 45 | case map[string]interface{}: 46 | i := Dict((*d)[name].(map[string]interface{})) 47 | return &i 48 | } 49 | return nil 50 | } 51 | 52 | func (d *Dict) GetArrayItem(name string, index int) *Dict { 53 | switch (*d)[name].(type) { 54 | case []interface{}: 55 | l := (*d)[name].([]interface{}) 56 | if len(l) > index { 57 | i := Dict(l[index].(map[string]interface{})) 58 | return &i 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | func (d *Dict) GetString(name string, def string) string { 65 | switch (*d)[name].(type) { 66 | case string: 67 | return (*d)[name].(string) 68 | } 69 | return def 70 | } 71 | 72 | func (d *Dict) GetFloat(name string, def float64) float64 { 73 | switch (*d)[name].(type) { 74 | case float64: 75 | return (*d)[name].(float64) 76 | } 77 | return def 78 | } 79 | 80 | func (d *Dict) GetUint8(name string, def uint8) uint8 { 81 | switch (*d)[name].(type) { 82 | case float64: 83 | x := uint8((*d)[name].(float64)) 84 | // stupid check, but in some cases the output may be zero 85 | if x != def { 86 | return x 87 | } 88 | } 89 | return def 90 | } 91 | 92 | func (d *Dict) GetUint16(name string, def uint16) uint16 { 93 | switch (*d)[name].(type) { 94 | case float64: 95 | x := uint16((*d)[name].(float64)) 96 | // stupid check, but in some cases the output may be zero 97 | if x != def { 98 | return x 99 | } 100 | } 101 | return def 102 | } 103 | 104 | func (d *Dict) GetUint32(name string, def uint32) uint32 { 105 | switch (*d)[name].(type) { 106 | case float64: 107 | return uint32((*d)[name].(float64)) 108 | } 109 | return def 110 | } 111 | 112 | func (d *Dict) GetUint64(name string, def uint64) uint64 { 113 | switch (*d)[name].(type) { 114 | case float64: 115 | return uint64((*d)[name].(float64)) 116 | } 117 | return def 118 | } 119 | -------------------------------------------------------------------------------- /gap/parsers.go: -------------------------------------------------------------------------------- 1 | package gap 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | ) 7 | 8 | type Map map[string]interface{} 9 | 10 | func (m Map) IsEvent() bool { 11 | _, ok := m["action"] 12 | return ok 13 | } 14 | 15 | func ParseATC1441(b []byte) Map { 16 | // without len, 0x16 and 0x181A 17 | switch len(b) { 18 | case 13: // atc1441 19 | return Map{ 20 | "temperature": float32(int16(binary.BigEndian.Uint16(b[6:]))) / 10, 21 | "humidity": float32(b[8]), 22 | "battery": b[9], 23 | "voltage": binary.BigEndian.Uint16(b[10:]), 24 | "seq": b[12], 25 | } 26 | case 15: // pvvx 27 | return Map{ 28 | "temperature": float32(int16(binary.LittleEndian.Uint16(b[6:]))) / 100, 29 | "humidity": float32(binary.LittleEndian.Uint16(b[8:])) / 100, 30 | "voltage": binary.LittleEndian.Uint16(b[10:]), 31 | "battery": b[12], 32 | "seq": b[13], 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | // ParseMiScalesV1 39 | // https://github.com/G1K/EspruinoHub/blob/3f3946206b81ea700493621f61c6d6e380b4ff0d/lib/attributes.js#L104 40 | func ParseMiScalesV1(b []byte) Map { 41 | if len(b) < 3 { 42 | return nil 43 | } 44 | 45 | result := Map{ 46 | "action": "weight", 47 | "stabilized": b[0]&0b100000 > 0, 48 | "removed": b[0]&0b10000000 > 0, 49 | } 50 | 51 | weight := float32(binary.LittleEndian.Uint16(b[1:])) 52 | 53 | switch { 54 | case b[0]&0b10000 > 0: 55 | result["weight"] = weight / 100 56 | case b[0]&0b1 > 0: 57 | result["weight_lb"] = weight / 100 58 | default: 59 | result["weight_kg"] = weight / 200 60 | } 61 | 62 | return result 63 | } 64 | 65 | // ParseMiScalesV2 66 | // https://github.com/G1K/EspruinoHub/blob/3f3946206b81ea700493621f61c6d6e380b4ff0d/lib/attributes.js#L78 67 | func ParseMiScalesV2(b []byte) Map { 68 | if len(b) < 12 { 69 | return nil 70 | } 71 | 72 | result := Map{ 73 | "action": "weight", 74 | "stabilized": b[1]&0b100000 > 0, 75 | "removed": b[1]&0b10000000 > 0, 76 | } 77 | 78 | if b[1]&0b10 > 0 { 79 | result["impedance"] = binary.LittleEndian.Uint16(b[9:]) 80 | } 81 | 82 | weight := float32(binary.LittleEndian.Uint16(b[11:])) 83 | 84 | switch b[0] { 85 | case 0b10000: 86 | result["weight"] = weight / 100 87 | case 3: 88 | result["weight_lb"] = weight / 100 89 | case 2: 90 | result["weight_kg"] = weight / 200 91 | } 92 | 93 | return result 94 | } 95 | 96 | func ParseIBeacon(b []byte) Map { 97 | // https://support.kontakt.io/hc/en-gb/articles/201492492-iBeacon-advertising-packet-structure 98 | // 0 0x02 99 | // 1 0x15 100 | // 2..17 UUID (16 bytes) 101 | // 18 19 Major 102 | // 20 21 Minor 103 | // 22 Power 104 | if len(b) != 23 || b[0] != 0x02 || b[1] != 0x15 { 105 | return nil 106 | } 107 | 108 | return Map{ 109 | "uuid": hex.EncodeToString(b[2:18]), 110 | "major": binary.BigEndian.Uint16(b[18:]), 111 | "minor": binary.BigEndian.Uint16(b[20:]), 112 | "tx": int8(b[22]), 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /gap/gap.go: -------------------------------------------------------------------------------- 1 | package gap 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | ) 9 | 10 | type Message struct { 11 | PacketType byte `json:"type"` 12 | MAC string `json:"mac"` 13 | Rand byte `json:"rand"` 14 | RSSI int8 `json:"rssi"` 15 | 16 | Brand string `json:"brand,omitempty"` 17 | Name string `json:"name,omitempty"` 18 | Comment string `json:"comment,omitempty"` 19 | Useful byte `json:"useful"` 20 | 21 | // https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf 22 | ServiceUUID uint16 `json:"uuid,omitempty"` 23 | // https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/ 24 | CompanyID uint16 `json:"cid,omitempty"` 25 | Raw map[byte]hexbytes `json:"raw,omitempty"` 26 | 27 | Data Map `json:"data,omitempty"` 28 | } 29 | 30 | type hexbytes []byte 31 | 32 | func (h hexbytes) MarshalJSON() ([]byte, error) { 33 | return json.Marshal(hex.EncodeToString(h)) 34 | } 35 | 36 | type Service struct { 37 | UUID string `json:"uuid"` 38 | } 39 | 40 | // Brands 41 | // https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/ 42 | var Brands = map[uint16]string{ 43 | 0x0006: "Microsoft", 44 | 0x004C: "Apple", 45 | 0x0075: "Samsung", 46 | 0x00E0: "Google", 47 | 0x0157: "Huami", 48 | 0x05A7: "Sonos", 49 | } 50 | 51 | func SprintMAC(b []byte) string { 52 | return fmt.Sprintf("%02X:%02X:%02X:%02X:%02X:%02X", b[5], b[4], b[3], b[2], b[1], b[0]) 53 | } 54 | 55 | func ParseScanResponse(data []byte) *Message { 56 | msg := &Message{ 57 | PacketType: data[5], 58 | MAC: SprintMAC(data[6:12]), 59 | Rand: data[12], 60 | RSSI: int8(data[4]), 61 | Raw: make(map[byte]hexbytes), 62 | } 63 | 64 | data = data[15:] 65 | 66 | var i int 67 | for i < len(data) { 68 | // 1 byte | len 69 | // 1 byte | advType 70 | // 2 byte | serviceID (advType=0x16) or company ID (advType=0xFF) 71 | // X byte | other data (len-3 bytes) 72 | l := int(data[i]) 73 | if l < 2 || i+l >= len(data) { 74 | msg.Comment = "wrong len" 75 | msg.Useful = 0 76 | return msg 77 | } 78 | 79 | advType := data[i+1] 80 | if (0x2D < advType && advType < 0xFF) || advType == 0 { 81 | msg.Comment = "wrong adv type" 82 | msg.Useful = 0 83 | return msg 84 | } 85 | msg.Raw[advType] = data[i+2 : i+l+1] 86 | 87 | switch advType { 88 | case 0x08, 0x09: 89 | msg.Name = string(data[i+2 : i+l+1]) 90 | case 0x16: 91 | msg.ServiceUUID = binary.LittleEndian.Uint16(data[i+2:]) 92 | switch msg.ServiceUUID { 93 | case 0xFE95: 94 | msg.Brand = "Xiaomi" 95 | msg.Useful = 1 96 | case 0xFE9F: 97 | msg.Brand = "Google" 98 | msg.Useful = 0 99 | default: 100 | msg.Useful = 1 101 | } 102 | case 0x2A: 103 | msg.Comment = "Mesh Message" 104 | msg.Useful = 0 105 | case 0x2B: 106 | msg.Comment = "Mesh Beacon" 107 | msg.Useful = 0 108 | case 0xFF: 109 | msg.CompanyID = binary.LittleEndian.Uint16(data[i+2:]) 110 | if val, ok := Brands[msg.CompanyID]; ok { 111 | msg.Brand = val 112 | } 113 | msg.Useful = 1 114 | } 115 | 116 | i += 1 + l 117 | } 118 | if i != len(data) { 119 | msg.Comment = "wrong len" 120 | } 121 | return msg 122 | } 123 | -------------------------------------------------------------------------------- /device_gateway.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "github.com/AlexxIT/gw3/dict" 6 | "github.com/rs/zerolog/log" 7 | ) 8 | 9 | type GatewayDevice struct { 10 | Type string `json:"type"` 11 | FwVersion string `json:"fw_version,omitempty"` 12 | Gw3 struct { 13 | Version string `json:"version,omitempty"` 14 | } `json:"gw3"` 15 | Miio struct { 16 | Did string `json:"did,omitempty"` 17 | } `json:"miio"` 18 | WiFi struct { 19 | MAC string `json:"mac,omitempty"` 20 | Addr string `json:"addr,omitempty"` 21 | } `json:"wifi"` 22 | BT struct { 23 | Addr uint16 `json:"addr,omitempty"` 24 | FwVersion string `json:"fw_version,omitempty"` 25 | IVIndex uint32 `json:"ivi"` 26 | } `json:"bt"` 27 | state dict.Dict 28 | alarmState string 29 | } 30 | 31 | func newGatewayDevice() *GatewayDevice { 32 | did, mac := shellDeviceInfo() 33 | 34 | device := &GatewayDevice{Type: "gateway", state: dict.Dict{}} 35 | device.Gw3.Version = version 36 | device.Miio.Did = did 37 | device.WiFi.MAC = mac 38 | devices[mac] = device 39 | mqttPublish("gw3/"+mac+"/info", device, true) 40 | return device 41 | } 42 | 43 | func (d *GatewayDevice) updateInfo() { 44 | mqttPublish("gw3/"+d.WiFi.MAC+"/info", d, true) 45 | } 46 | 47 | func (d *GatewayDevice) updateState(state string) { 48 | // skip same state 49 | if d.state["state"] == state { 50 | return 51 | } 52 | d.state["state"] = state 53 | mqttPublish("gw3/"+d.WiFi.MAC+"/state", d.state, true) 54 | } 55 | 56 | func (d *GatewayDevice) updateAlarmState(state string) { 57 | if state != "triggered" { 58 | if state == "" { 59 | // restore state after triggered 60 | state = d.alarmState 61 | } else { 62 | // remember state before triggered 63 | d.alarmState = state 64 | } 65 | } 66 | d.state["alarm_state"] = state 67 | mqttPublish("gw3/"+d.WiFi.MAC+"/state", d.state, true) 68 | } 69 | 70 | func (d *GatewayDevice) updateEvent(data *dict.Dict) { 71 | mqttPublish("gw3/"+d.WiFi.MAC+"/event", data, false) 72 | } 73 | 74 | func (d *GatewayDevice) updateBT(fw string, addr uint16, ivi uint32) { 75 | d.BT.FwVersion = fw 76 | d.BT.Addr = addr 77 | d.BT.IVIndex = ivi 78 | mqttPublish("gw3/"+d.WiFi.MAC+"/info", d, true) 79 | } 80 | 81 | func (d *GatewayDevice) getState() { 82 | // BLE device can't get state 83 | } 84 | 85 | func (d *GatewayDevice) setState(p []byte) { 86 | payload, err := dict.Unmarshal(p) 87 | if err != nil { 88 | log.Warn().Err(err).Send() 89 | return 90 | } 91 | 92 | if value, ok := payload.TryGetString("alarm_state"); ok { 93 | miioEncodeGatewayProps(value) 94 | } 95 | 96 | if value, ok := payload.TryGetString("buzzer"); ok { 97 | switch value { 98 | case "ON": 99 | duration := payload.GetUint64("duration", 1) 100 | volume := payload.GetUint8("volume", 3) 101 | miioEncodeGatewayBuzzer(duration, volume) 102 | case "OFF": 103 | miioEncodeGatewayBuzzer(0, 0) 104 | } 105 | } 106 | 107 | if value, ok := payload.TryGetString("log"); ok { 108 | mainInitLogger(value) 109 | } 110 | 111 | if value, ok := payload.TryGetString("test"); ok { 112 | switch value { 113 | case "error": 114 | // raise unhandled error 115 | devices["test"].(DeviceGetSet).getState() 116 | case "fatal": 117 | err = errors.New("test") 118 | log.Fatal().Caller().Err(err).Send() 119 | case "panic": 120 | err = errors.New("test") 121 | log.Panic().Err(err).Send() 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 3 | github.com/huin/gobinarytest v0.0.0-20170803182140-bc6c19e91749 h1:uEtvxEWZtnFRuUUyJl214tS14+BMi7iO7up/Rx4CejY= 4 | github.com/huin/gobinarytest v0.0.0-20170803182140-bc6c19e91749/go.mod h1:NLS5Is63wKCuzcJLN28yt3sm/Ps9OXNd9GPUlO4wfFA= 5 | github.com/huin/mqtt v0.0.0-20200914141616-61735481eb15 h1:BT2uKnsrOQvQzZjFIpDMMfHFV7O5nX7Sc5ekEJjojbM= 6 | github.com/huin/mqtt v0.0.0-20200914141616-61735481eb15/go.mod h1:zbO4h3NDGxpAVWGYzQNle+BDfuwm8fWcgVxFuIHyuas= 7 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 8 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 9 | github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II= 10 | github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI= 11 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 14 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 15 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 16 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 17 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 18 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 19 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 20 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 21 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 h1:7ZDGnxgHAMw7thfC5bEos0RDAccZKxioiWBhfIe+tvw= 26 | golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 28 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 29 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 30 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 31 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 32 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 33 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 34 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 35 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 36 | -------------------------------------------------------------------------------- /device_ble.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/AlexxIT/gw3/dict" 5 | "github.com/AlexxIT/gw3/gap" 6 | "github.com/rs/zerolog/log" 7 | ) 8 | 9 | type BLEDevice struct { 10 | Type string `json:"type"` 11 | Brand string `json:"brand"` 12 | Name string `json:"name"` 13 | Model string `json:"model"` 14 | MAC string `json:"mac"` 15 | state gap.Map 16 | } 17 | 18 | var brands = []string{ 19 | "atc1441", "Xiaomi", "TH Sensor ATC", "ATC1441", 20 | "miscales", "Xiaomi", "Mi Scale", "XMTZC01HM", 21 | "miscales2", "Xiaomi", "Mi Scale 2", "XMTZC04HM", 22 | "ibeacon", "Apple", "iBeacon", "Tracker", 23 | "nut", "NutFind", "Nut", "Tracker", 24 | "miband", "Xiaomi", "Mi Band", "Tracker", 25 | "mi:152", "Xiaomi", "Flower Care", "HHCCJCY01", 26 | "mi:131", "Xiaomi", "Kettle", "YM-K1501", // CH, HK, RU version 27 | "mi:275", "Xiaomi", "Kettle", "YM-K1501", // international 28 | "mi:339", "Yeelight", "Remote Control", "YLYK01YL", 29 | "mi:349", "Xiaomi", "Flower Pot", "HHCCPOT002", 30 | "mi:426", "Xiaomi", "TH Sensor", "LYWSDCGQ/01ZM", 31 | "mi:794", "Xiaomi", "Door Lock", "MJZNMS02LM", 32 | "mi:839", "Xiaomi", "Qingping TH Sensor", "CGG1", 33 | "mi:860", "Xiaomi", "Scooter M365 Pro", "DDHBC02NEB", // only tracking 34 | "mi:903", "Xiaomi", "ZenMeasure TH", "MHO-C401", 35 | "mi:950", "Yeelight", "Dimmer", "YLKG07YL", 36 | "mi:959", "Yeelight", "Heater Remote", "YLYB01YL-BHFRC", 37 | "mi:982", "Xiaomi", "Qingping Door Sensor", "CGH1", 38 | "mi:1034", "Xiaomi", "Mosquito Repellent", "WX08ZM", 39 | "mi:1115", "Xiaomi", "TH Clock", "LYWSD02MMC", 40 | "mi:1116", "Xiaomi", "Viomi Kettle", "V-SK152", // international 41 | "mi:1161", "Xiaomi", "Toothbrush T500", "MES601", 42 | "mi:1249", "Xiaomi", "Magic Cube", "XMMF01JQD", 43 | "mi:1254", "Yeelight", "Fan Remote", "YLYK01YL-VENFAN", 44 | "mi:1371", "Xiaomi", "TH Sensor 2", "LYWSD03MMC", 45 | "mi:1398", "Xiaomi", "Alarm Clock", "CGD1", 46 | "mi:1433", "Xiaomi", "Door Lock", "MJZNMS03LM", 47 | "mi:1647", "Xiaomi", "Qingping TH Lite", "CGDK2", 48 | "mi:1678", "Yeelight", "Fan Remote", "YLYK01YL-FANCL", 49 | "mi:1694", "Aqara", "Door Lock N100", "ZNMS16LM", 50 | "mi:1695", "Aqara", "Door Lock N200", "ZNMS17LM", 51 | "mi:1747", "Xiaomi", "ZenMeasure Clock", "MHO-C303", 52 | "mi:1983", "Yeelight", "Button S1", "YLAI003", 53 | "mi:2038", "Xiaomi", "Night Light 2", "MJYD02YL-A", // 15,4103,4106,4119,4120 54 | "mi:2147", "Xiaomi", "Water Leak Sensor", "SJWS01LM", 55 | "mi:2443", "Xiaomi", "Door Sensor 2", "MCCGQ02HL", 56 | "mi:2444", "Xiaomi", "Door Lock", "XMZNMST02YD", 57 | "mi:2455", "Honeywell", "Smoke Alarm", "JTYJ-GD-03MI", 58 | "mi:2480", "Xiaomi", "Safe Box", "BGX-5/X1-3001", 59 | "mi:2691", "Xiaomi", "Qingping Motion Sensor", "CGPR1", 60 | "mi:2701", "Xiaomi", "Motion Sensor 2", "RTCGQ02LM", // 15,4119,4120 61 | "mi:2888", "Xiaomi", "Qingping TH Sensor", "CGG1", // same model as 839?! 62 | } 63 | 64 | func newBLEDevice(mac string, advType string) *BLEDevice { 65 | device := &BLEDevice{ 66 | Type: "ble", 67 | MAC: mac, 68 | } 69 | 70 | for i := 0; i < len(brands); i += 4 { 71 | if advType == brands[i] { 72 | device.Brand = brands[i+1] 73 | device.Name = brands[i+2] 74 | device.Model = brands[i+3] 75 | break 76 | } 77 | } 78 | 79 | if device.Model == "" { 80 | device.Model = advType 81 | } 82 | 83 | devices[mac] = device 84 | mqttPublish("gw3/"+mac+"/info", device, true) 85 | return device 86 | } 87 | 88 | func (d *BLEDevice) updateState(data gap.Map) { 89 | if data.IsEvent() { 90 | mqttPublish("gw3/"+d.MAC+"/event", data, false) 91 | return 92 | } 93 | 94 | if d.state != nil { 95 | for k, v := range data { 96 | d.state[k] = v 97 | } 98 | } else { 99 | d.state = data 100 | } 101 | mqttPublish("gw3/"+d.MAC+"/state", d.state, true) 102 | } 103 | 104 | func (d *BLEDevice) getState() { 105 | // BLE device can't get state 106 | } 107 | 108 | func (d *BLEDevice) setState(p []byte) { 109 | payload, err := dict.Unmarshal(p) 110 | if err != nil { 111 | log.Warn().Err(err).Send() 112 | return 113 | } 114 | if value, ok := payload.TryGetString("bindkey"); ok { 115 | config.SetBindKey(d.MAC, value) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /shell.go: -------------------------------------------------------------------------------- 1 | /* 2 | /bin/mijia_automation /data/mijia_automation/db.unqlite 3 | /bin/mijia_automation /data/mijia_automation 4 | /bin/silabs_ncp_bt /data/miio/mible_local.db 5 | /bin/silabs_ncp_bt /data/miio/mible_local.db-wal 6 | /bin/silabs_ncp_bt /data/miio/mible_local.db-shm 7 | /bin/silabs_ncp_bt /data/ble_info 8 | /bin/miio_client /data/miioconfig.db 9 | /bin/miio_client /data/miioconfig.db_unqlite_journal 10 | /bin/basic_app /data/basic_app/gw_devices.data 11 | /bin/zigbee_agent /data/zigbee/coordinator.info 12 | /bin/zigbee_gw /data/zigbee_gw 13 | /bin/zigbee_gw /data/zigbee_gw/device_properties.json 14 | */ 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "github.com/rs/zerolog/log" 20 | "io/ioutil" 21 | "os" 22 | "os/exec" 23 | "strings" 24 | "time" 25 | ) 26 | 27 | func shellKillall(filename string) { 28 | _ = exec.Command("killall", filename).Run() 29 | } 30 | 31 | func shellFreeTTY() { 32 | out, err := exec.Command("sh", "-c", "lsof | grep ptyp8 | cut -f 1").Output() 33 | if err != nil { 34 | log.Panic().Err(err).Send() 35 | } 36 | if len(out) == 0 { 37 | return 38 | } 39 | 40 | // remove leading new line: "1234\n" 41 | pid := string(out[:len(out)-1]) 42 | log.Debug().Str("pid", pid).Msg("Releasing the TTY") 43 | _ = exec.Command("kill", pid).Run() 44 | } 45 | 46 | func shellUpdatePath() { 47 | _ = os.Setenv("PATH", "/tmp:"+os.Getenv("PATH")) 48 | } 49 | 50 | func shellRunDaemon() { 51 | log.Debug().Msg("Run daemon_miio.sh") 52 | // run patched script without error processing 53 | _ = exec.Command("sh", "-c", "daemon_miio.sh&").Run() 54 | } 55 | 56 | func shellRunMosquitto() bool { 57 | if out, err := exec.Command("sh", "-c", "ps | grep mosquitto").Output(); err == nil { 58 | if bytes.Contains(out, []byte("mosquitto -d")) { 59 | return false 60 | } 61 | } else { 62 | log.Fatal().Err(err).Send() 63 | } 64 | 65 | log.Debug().Msg("Run public mosquitto") 66 | 67 | shellKillall("mosquitto") 68 | 69 | time.Sleep(time.Second) 70 | 71 | _ = exec.Command("mosquitto", "-d").Run() 72 | 73 | return true 74 | } 75 | 76 | func shellDeviceInfo() (did string, mac string) { 77 | // did=123456789 78 | // key=xxxxxxxxxxxxxxxx 79 | // mac=54:EF:44:FF:FF:FF 80 | // vendor=lumi 81 | // model=lumi.gateway.mgl03 82 | data, err := ioutil.ReadFile("/data/miio/device.conf") 83 | if err != nil { 84 | log.Panic().Err(err).Send() 85 | } 86 | for _, line := range strings.Split(string(data), "\n") { 87 | if len(line) < 5 { 88 | continue 89 | } 90 | switch line[:3] { 91 | case "did": 92 | did = line[4:] 93 | case "mac": 94 | mac = line[4:] 95 | } 96 | } 97 | return 98 | } 99 | 100 | var shellPatchTimer *time.Timer 101 | 102 | func shellPatchTimerStart() { 103 | if config.patchDelay == 0 { 104 | return 105 | } 106 | if _, err := os.Stat("/tmp/silabs_ncp_bt"); !os.IsNotExist(err) { 107 | return 108 | } 109 | if shellPatchTimer == nil { 110 | log.Debug().Msg("Start patch timer") 111 | shellPatchTimer = time.AfterFunc(config.patchDelay, func() { 112 | shellPatchApp("silabs_ncp_bt") 113 | // we need to restart daemon because new binary in tmp path 114 | shellKillall("daemon_miio.sh") 115 | shellKillall("silabs_ncp_bt") 116 | shellRunDaemon() 117 | shellPatchTimer = nil 118 | }) 119 | } else { 120 | log.Debug().Msg("Reset patch timer") 121 | shellPatchTimer.Reset(config.patchDelay) 122 | } 123 | } 124 | 125 | func shellPatchTimerStop() { 126 | if shellPatchTimer != nil { 127 | log.Debug().Msg("Stop patch timer") 128 | shellPatchTimer.Stop() 129 | } 130 | } 131 | 132 | func shellPatchApp(filename string) bool { 133 | if _, err := os.Stat("/tmp/" + filename); !os.IsNotExist(err) { 134 | return false 135 | } 136 | 137 | log.Info().Str("file", filename).Msg("Patch app") 138 | 139 | // read original file (firmware v1.4.7_0063+) 140 | data, err := ioutil.ReadFile("/bin/" + filename) 141 | if err != nil { 142 | data, err = ioutil.ReadFile("/usr/app/bin/" + filename) 143 | if err != nil { 144 | log.Panic().Err(err).Send() 145 | } 146 | } 147 | 148 | switch filename { 149 | case "daemon_miio.sh": 150 | // silabs_ncp_bt will work with out proxy-TTY 151 | data = bytes.Replace(data, []byte("ttyS1"), []byte("ttyp8"), 1) 152 | // old fimware v1.4.6 153 | data = bytes.Replace(data, []byte("$APP_PATH/"), []byte(""), -1) 154 | case "silabs_ncp_bt": 155 | // Zigbee and Bluetooth data is broken when writing to NAND. So we moving sqlite database to memory (tmp). 156 | // It's not a problem to lose this base, because the gateway will restore it from the cloud. 157 | data = bytes.Replace(data, []byte("/data/"), []byte("/tmp//"), -1) 158 | 159 | // copy databases 160 | _ = exec.Command("cp", "-R", "/data/miio", "/data/ble_info", "/tmp/").Run() 161 | case "miio_agent": 162 | // miio_agent will work with out proxy-socket 163 | data = bytes.Replace(data, []byte("/tmp/miio_agent.socket"), []byte("/tmp/true_agent.socket"), -1) 164 | } 165 | 166 | // write patched script 167 | if err = ioutil.WriteFile("/tmp/"+filename, data, 0x777); err != nil { 168 | log.Panic().Err(err).Send() 169 | } 170 | 171 | return true 172 | } 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gw3 2 | 3 | [![Donate](https://img.shields.io/badge/donate-BuyMeCoffee-yellow.svg)](https://www.buymeacoffee.com/AlexxIT) 4 | [![Donate](https://img.shields.io/badge/donate-YooMoney-8C3FFD.svg)](https://yoomoney.ru/to/41001428278477) 5 | 6 | Standalone application for integrating **Xiaomi Mijia Smart Multi-Mode Gateway (aka Xiaomi Gateway 3)** into an open source Smart Home platforms. 7 | 8 | The application runs directly on the gateway and converts data from surrounding BLE devices into MQTT-topics. All default gateway functionality continues to work as usual in the Mi Home ecosystem. 9 | 10 | The app only needs the Internet to download the encryption keys for Xiaomi devices. The rest of the time, the app works without the Internet. 11 | 12 | ## Supported BLE Devices 13 | 14 | BLE devices are not linked to any specific gateway. Their data can be received by multiple gateways at the same time. Remember about the distance between the device and the gateway. 15 | 16 | **Mi Home Devices** 17 | 18 | All Xiaomi devices must be linked to an account via the Mi Home app, otherwise they don't send data. Some Xioami devices have encryption. The app will automatically retrieve encryption keys for your devices from the cloud. The app will display all unencrypted devices, even if they are connected to another account. 19 | 20 | - Aqara Door Lock N100 (ZNMS16LM) 21 | - Aqara Door Lock N200 (ZNMS17LM) 22 | - Honeywell Smoke Alarm (JTYJ-GD-03MI) 23 | - Xiaomi Alarm Clock (CGD1) 24 | - Xiaomi Door Lock (MJZNMS02LM,XMZNMST02YD) 25 | - Xiaomi Door Sensor 2 (MCCGQ02HL) 26 | - Xiaomi Flower Care (HHCCJCY01) 27 | - Xiaomi Flower Pot (HHCCPOT002) 28 | - Xiaomi Kettle (YM-K1501) 29 | - Xiaomi Magic Cube (XMMF01JQD) - doesn't sends edge info, only direction! 30 | - Xiaomi Mosquito Repellent (WX08ZM) 31 | - Xiaomi Motion Sensor 2 (RTCGQ02LM) 32 | - Xiaomi Night Light 2 (MJYD02YL-A) 33 | - Xiaomi Qingping Door Sensor (CGH1) 34 | - Xiaomi Qingping Motion Sensor (CGPR1) 35 | - Xiaomi Qingping TH Lite (CGDK2) 36 | - Xiaomi Qingping TH Sensor (CGG1) 37 | - Xiaomi Safe Box (BGX-5/X1-3001) 38 | - Xiaomi TH Clock (LYWSD02MMC) 39 | - Xiaomi TH Sensor (LYWSDCGQ/01ZM) 40 | - Xiaomi TH Sensor 2 (LYWSD03MMC) - also supports custom [atc1441](https://github.com/atc1441/ATC_MiThermometer) and [pvvx](https://github.com/pvvx/ATC_MiThermometer) firmwares 41 | - Xiaomi Toothbrush T500 (MES601) 42 | - Xiaomi Viomi Kettle (V-SK152) 43 | - Xiaomi Water Leak Sensor (SJWS01LM) 44 | - Xiaomi ZenMeasure Clock (MHO-C303) 45 | - Xiaomi ZenMeasure TH (MHO-C401) 46 | - Yeelight Button S1 (YLAI003) 47 | 48 | **Other Xiaomi Devices** 49 | 50 | - Xiaomi Mi Scale (XMTZC01HM) 51 | - Xiaomi Mi Scale 2 (XMTZC04HM) 52 | - Yeelight Dimmer (YLKG07YL) - works awful, skips a lot of click 53 | - Yeelight Heater Remote (YLYB01YL-BHFRC) 54 | - Yeelight Remote Control (YLYK01YL) 55 | 56 | **Person Trackers** 57 | 58 | - [iBeacon](https://en.wikipedia.org/wiki/IBeacon) - example [Home Assistant for Android](https://companion.home-assistant.io/docs/core/sensors#bluetooth-sensors) **BLE Transmitter** feature 59 | - [NutFind Nut](https://www.nutfind.com/) - must be unlinked from the phone 60 | - Amazfit Watch - the detection function must be enabled, it may be enabled not on all accounts 61 | - Xiaomi Mi Band - the detection function must be enabled, enabled by default on some models 62 | 63 | ## Using with Home Assistant 64 | 65 | Just install [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3) integration. It will do all the magic for you. 66 | 67 | ## Manual installation 68 | 69 | If you are not an IT guy, just use Home Assistant. Seriously, why do you need all this trouble? 70 | 71 | If you are still here: 72 | 73 | 1. Open [Telnet](https://gist.github.com/zvldz/1bd6b21539f84339c218f9427e022709) on the gateway. 74 | 2. Install [custom firmware](https://github.com/zvldz/mgl03_fw/tree/main/firmware) 75 | - it will open telnet forever (until the next firmware update) 76 | - it will run public MQTT on gateway (without auth) 77 | - it will allow you to run your startup script 78 | 3. Download lastest [gw3 binary](https://sourceforge.net/projects/mgl03/files/bin/) on gateway: 79 | ```shell 80 | wget "http://master.dl.sourceforge.net/project/mgl03/bin/gw3?viasf=1" -O /data/gw3 && chmod +x /data/gw3 81 | ``` 82 | 4. Add your startup script `/data/run.sh` on gateway: 83 | ```shell 84 | echo "/data/gw3 -log=syslog,info 2>&1 | mosquitto_pub -t gw3/stderr -s &" > /data/run.sh && chmod +x /data/run.sh 85 | ``` 86 | 5. Reboot gateway 87 | 88 | **PS:** gw3 binary can run on the original gateway firmware. Custom firmware is an optional step that just makes your life easier. Custom firmware doesn't change default gateway functionality in Mi Home ecosystem. 89 | 90 | ## Debug 91 | 92 | - Support levels: `debug`, `info`, warn (default) 93 | - Support output: `syslog`, `mqtt`, stdout (default) 94 | - Support format: `text` (nocolor), `json`, color (default) 95 | - Support more debug: `btraw`, `btgap`, `miio` 96 | 97 | Example: debug logs in json format to MQTT: 98 | 99 | ```shell 100 | /data/gw3 -log=mqtt,json,debug 101 | ``` 102 | 103 | Or change logs while app running: 104 | 105 | ```shell 106 | mosquitto_pub -t gw3/AA:BB:CC:DD:EE:FF/set -m '{"log":"syslog,info,text"}' 107 | ``` 108 | -------------------------------------------------------------------------------- /gw3.go: -------------------------------------------------------------------------------- 1 | /** 2 | log.Panic().Err(err).Send() - output to default and stderr with trace and exit app 3 | log.Fatal() - output to default and exit app, useless! 4 | log.Error().Caller().Err(err).Send() - output to default with line number 5 | */ 6 | package main 7 | 8 | import ( 9 | "encoding/json" 10 | "flag" 11 | "github.com/rs/zerolog" 12 | "github.com/rs/zerolog/log" 13 | "io" 14 | "io/ioutil" 15 | "log/syslog" 16 | "os" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | var ( 22 | config = &Config{} 23 | devices = make(map[string]interface{}) 24 | gw = newGatewayDevice() 25 | version string 26 | ) 27 | 28 | func main() { 29 | mainInitConfig() 30 | 31 | shellUpdatePath() 32 | 33 | // kill daemon_miio.sh before kill silabs_ncp_bt 34 | shellKillall("daemon_miio.sh") 35 | // kill silabs_ncp_bt before open TTY 36 | shellKillall("silabs_ncp_bt") 37 | 38 | btappInit() 39 | btchipInit() 40 | 41 | // need to restart zigbee_gw after restart mosquitto 42 | if shellRunMosquitto() { 43 | shellKillall("zigbee_gw") 44 | } 45 | 46 | // patch daemon_miio.sh if needed 47 | shellPatchApp("daemon_miio.sh") 48 | 49 | // run daemon_miio.sh what runs other apps from /tmp or /bin 50 | shellRunDaemon() 51 | 52 | go miioReader() 53 | go mqttReader() 54 | 55 | go btchipReader() 56 | go btappReader() 57 | 58 | select {} // run forever 59 | } 60 | 61 | func mainInitConfig() { 62 | v := flag.Bool("v", false, "Prints current version") 63 | 64 | logs := flag.String("log", "", 65 | "Logs modes: debug,info + btraw,btgap,miio + syslog,mqtt + json,text") 66 | 67 | flag.DurationVar(&config.discoveryDelay, "dd", time.Minute, "BLE discovery delay") 68 | flag.DurationVar(&config.patchDelay, "pd", 5*time.Minute, "Silabs patch delay, 0 - disabled") 69 | 70 | flag.Parse() 71 | 72 | if *v { 73 | println(version) 74 | os.Exit(0) 75 | } 76 | 77 | if data, err := ioutil.ReadFile("/data/gw3.json"); err == nil { 78 | if err = json.Unmarshal(data, config); err != nil { 79 | log.Panic().Err(err).Send() 80 | } 81 | } 82 | 83 | mainInitLogger(*logs) 84 | } 85 | 86 | // additional log levels for advanced output 87 | var btraw, btgap, btskip, miioraw zerolog.Level 88 | 89 | // log levels: debug, info, warn (default) 90 | // advanced debug: 91 | // - btraw - all BT raw data except GAP 92 | // - btgap - only BT GAP raw data 93 | // - miio - miio raw data 94 | // log out: syslog, mqtt, stdout (default) 95 | // log format: json, text (nocolor), console (default) 96 | func mainInitLogger(logs string) { 97 | if strings.Contains(logs, "debug") { 98 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 99 | } else if strings.Contains(logs, "info") { 100 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 101 | } else { 102 | zerolog.SetGlobalLevel(zerolog.WarnLevel) 103 | } 104 | 105 | if strings.Contains(logs, "btraw") { 106 | btraw = zerolog.NoLevel 107 | } else { 108 | btraw = zerolog.Disabled 109 | } 110 | if strings.Contains(logs, "btgap") { 111 | btgap = zerolog.NoLevel 112 | } else { 113 | btgap = zerolog.Disabled 114 | } 115 | if strings.Contains(logs, "btskip") { 116 | btskip = zerolog.Disabled 117 | } else { 118 | btskip = zerolog.WarnLevel 119 | } 120 | if strings.Contains(logs, "miio") { 121 | miioraw = zerolog.NoLevel 122 | } else { 123 | miioraw = zerolog.Disabled 124 | } 125 | 126 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs 127 | 128 | var writer io.Writer 129 | if strings.Contains(logs, "syslog") { 130 | var err error 131 | writer, err = syslog.New(syslog.LOG_USER|syslog.LOG_NOTICE, "gw3") 132 | if err != nil { 133 | log.Panic().Err(err).Send() 134 | } 135 | } else if strings.Contains(logs, "mqtt") { 136 | writer = mqttLogWriter{} 137 | } else { 138 | writer = os.Stdout 139 | } 140 | if !strings.Contains(logs, "json") { 141 | nocolor := writer != os.Stdout || strings.Contains(logs, "text") 142 | writer = zerolog.ConsoleWriter{Out: writer, TimeFormat: "15:04:05.000", NoColor: nocolor} 143 | } 144 | log.Logger = log.Output(writer) 145 | } 146 | 147 | type Config struct { 148 | Devices map[string]ConfigDevice `json:"devices,omitempty"` 149 | discoveryDelay time.Duration 150 | patchDelay time.Duration 151 | } 152 | 153 | type ConfigDevice struct { 154 | Bindkey string `json:"bindkey,omitempty"` 155 | } 156 | 157 | func (c *Config) GetBindkey(mac string) string { 158 | if device, ok := c.Devices[mac]; ok { 159 | return device.Bindkey 160 | } 161 | return "" 162 | } 163 | 164 | func (c *Config) SetBindKey(mac string, bindkey string) { 165 | if c.Devices == nil { 166 | c.Devices = make(map[string]ConfigDevice) 167 | } 168 | if device, ok := c.Devices[mac]; ok { 169 | if device.Bindkey == bindkey { 170 | // skip if nothing changed 171 | return 172 | } 173 | device.Bindkey = bindkey 174 | } else { 175 | c.Devices[mac] = ConfigDevice{Bindkey: bindkey} 176 | } 177 | 178 | data, err := json.Marshal(c) 179 | if err != nil { 180 | log.Error().Caller().Err(err).Send() 181 | return 182 | } 183 | log.Info().Str("mac", mac).Msg("Write new bindkey to config") 184 | 185 | if err = ioutil.WriteFile("/data/gw3.json", data, 0666); err != nil { 186 | log.Error().Caller().Err(err).Send() 187 | } 188 | } 189 | 190 | type DeviceGetSet interface { 191 | getState() 192 | setState(p []byte) 193 | } 194 | -------------------------------------------------------------------------------- /serial/open_linux.go: -------------------------------------------------------------------------------- 1 | package serial 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "syscall" 8 | "unsafe" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | // 14 | // Grab the constants with the following little program, to avoid using cgo: 15 | // 16 | // #include 17 | // #include 18 | // #include 19 | // 20 | // int main(int argc, const char **argv) { 21 | // printf("TCSETS2 = 0x%08X\n", TCSETS2); 22 | // printf("BOTHER = 0x%08X\n", BOTHER); 23 | // printf("NCCS = %d\n", NCCS); 24 | // return 0; 25 | // } 26 | // 27 | 28 | // https://github.com/jacobsa/go-serial/issues/27 29 | const ( 30 | kTCSETS2 = unix.TCSETS2 // 0x8030542B 31 | kBOTHER = unix.BOTHER // 0x00001000 32 | kNCCS = 23 // 23 is the value fix for MIPS. most of the OpenWrt routers installed with MIPS cpu. 33 | ) 34 | 35 | // 36 | // Types from asm-generic/termbits.h 37 | // 38 | 39 | type cc_t byte 40 | type speed_t uint32 41 | type tcflag_t uint32 42 | type termios2 struct { 43 | c_iflag tcflag_t // input mode flags 44 | c_oflag tcflag_t // output mode flags 45 | c_cflag tcflag_t // control mode flags 46 | c_lflag tcflag_t // local mode flags 47 | c_line cc_t // line discipline 48 | c_cc [kNCCS]cc_t // control characters 49 | c_ispeed speed_t // input speed 50 | c_ospeed speed_t // output speed 51 | } 52 | 53 | // Constants for RS485 operation 54 | 55 | const ( 56 | sER_RS485_ENABLED = (1 << 0) 57 | sER_RS485_RTS_ON_SEND = (1 << 1) 58 | sER_RS485_RTS_AFTER_SEND = (1 << 2) 59 | sER_RS485_RX_DURING_TX = (1 << 4) 60 | tIOCSRS485 = 0x542F 61 | ) 62 | 63 | type serial_rs485 struct { 64 | flags uint32 65 | delay_rts_before_send uint32 66 | delay_rts_after_send uint32 67 | padding [5]uint32 68 | } 69 | 70 | // 71 | // Returns a pointer to an instantiates termios2 struct, based on the given 72 | // OpenOptions. Termios2 is a Linux extension which allows arbitrary baud rates 73 | // to be specified. 74 | // 75 | func makeTermios2(options OpenOptions) (*termios2, error) { 76 | 77 | // Sanity check inter-character timeout and minimum read size options. 78 | 79 | vtime := uint(round(float64(options.InterCharacterTimeout)/100.0) * 100) 80 | vmin := options.MinimumReadSize 81 | 82 | if vmin == 0 && vtime < 100 { 83 | return nil, errors.New("invalid values for InterCharacterTimeout and MinimumReadSize") 84 | } 85 | 86 | if vtime > 25500 { 87 | return nil, errors.New("invalid value for InterCharacterTimeout") 88 | } 89 | 90 | ccOpts := [kNCCS]cc_t{} 91 | ccOpts[syscall.VTIME] = cc_t(vtime / 100) 92 | ccOpts[syscall.VMIN] = cc_t(vmin) 93 | 94 | t2 := &termios2{ 95 | c_cflag: syscall.CLOCAL | syscall.CREAD | kBOTHER, 96 | c_ispeed: speed_t(options.BaudRate), 97 | c_ospeed: speed_t(options.BaudRate), 98 | c_cc: ccOpts, 99 | } 100 | 101 | switch options.StopBits { 102 | case 1: 103 | case 2: 104 | t2.c_cflag |= syscall.CSTOPB 105 | 106 | default: 107 | return nil, errors.New("invalid setting for StopBits") 108 | } 109 | 110 | switch options.ParityMode { 111 | case PARITY_NONE: 112 | case PARITY_ODD: 113 | t2.c_cflag |= syscall.PARENB 114 | t2.c_cflag |= syscall.PARODD 115 | 116 | case PARITY_EVEN: 117 | t2.c_cflag |= syscall.PARENB 118 | 119 | default: 120 | return nil, errors.New("invalid setting for ParityMode") 121 | } 122 | 123 | switch options.DataBits { 124 | case 5: 125 | t2.c_cflag |= syscall.CS5 126 | case 6: 127 | t2.c_cflag |= syscall.CS6 128 | case 7: 129 | t2.c_cflag |= syscall.CS7 130 | case 8: 131 | t2.c_cflag |= syscall.CS8 132 | default: 133 | return nil, errors.New("invalid setting for DataBits") 134 | } 135 | 136 | if options.RTSCTSFlowControl { 137 | t2.c_cflag |= unix.CRTSCTS 138 | } 139 | 140 | return t2, nil 141 | } 142 | 143 | func openInternal(options OpenOptions) (io.ReadWriteCloser, error) { 144 | 145 | file, openErr := 146 | os.OpenFile( 147 | options.PortName, 148 | syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK, 149 | 0600) 150 | if openErr != nil { 151 | return nil, openErr 152 | } 153 | 154 | // Clear the non-blocking flag set above. 155 | nonblockErr := syscall.SetNonblock(int(file.Fd()), false) 156 | if nonblockErr != nil { 157 | return nil, nonblockErr 158 | } 159 | 160 | t2, optErr := makeTermios2(options) 161 | if optErr != nil { 162 | return nil, optErr 163 | } 164 | 165 | r, _, errno := syscall.Syscall( 166 | syscall.SYS_IOCTL, 167 | uintptr(file.Fd()), 168 | uintptr(kTCSETS2), 169 | uintptr(unsafe.Pointer(t2))) 170 | 171 | if errno != 0 { 172 | return nil, os.NewSyscallError("SYS_IOCTL", errno) 173 | } 174 | 175 | if r != 0 { 176 | return nil, errors.New("unknown error from SYS_IOCTL") 177 | } 178 | 179 | if options.Rs485Enable { 180 | rs485 := serial_rs485{ 181 | sER_RS485_ENABLED, 182 | uint32(options.Rs485DelayRtsBeforeSend), 183 | uint32(options.Rs485DelayRtsAfterSend), 184 | [5]uint32{0, 0, 0, 0, 0}, 185 | } 186 | 187 | if options.Rs485RtsHighDuringSend { 188 | rs485.flags |= sER_RS485_RTS_ON_SEND 189 | } 190 | 191 | if options.Rs485RtsHighAfterSend { 192 | rs485.flags |= sER_RS485_RTS_AFTER_SEND 193 | } 194 | 195 | r, _, errno := syscall.Syscall( 196 | syscall.SYS_IOCTL, 197 | uintptr(file.Fd()), 198 | uintptr(tIOCSRS485), 199 | uintptr(unsafe.Pointer(&rs485))) 200 | 201 | if errno != 0 { 202 | return nil, os.NewSyscallError("SYS_IOCTL (RS485)", errno) 203 | } 204 | 205 | if r != 0 { 206 | return nil, errors.New("Unknown error from SYS_IOCTL (RS485)") 207 | } 208 | } 209 | 210 | return file, nil 211 | } 212 | -------------------------------------------------------------------------------- /serial/serial.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Aaron Jacobs. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package serial provides routines for interacting with serial ports. 16 | // Currently it supports only OS X; see the readme file for details. 17 | 18 | package serial 19 | 20 | import ( 21 | "io" 22 | "math" 23 | ) 24 | 25 | // Valid parity values. 26 | type ParityMode int 27 | 28 | const ( 29 | PARITY_NONE ParityMode = 0 30 | PARITY_ODD ParityMode = 1 31 | PARITY_EVEN ParityMode = 2 32 | ) 33 | 34 | var ( 35 | // The list of standard baud-rates. 36 | StandardBaudRates = map[uint]bool{ 37 | 50: true, 38 | 75: true, 39 | 110: true, 40 | 134: true, 41 | 150: true, 42 | 200: true, 43 | 300: true, 44 | 600: true, 45 | 1200: true, 46 | 1800: true, 47 | 2400: true, 48 | 4800: true, 49 | 7200: true, 50 | 9600: true, 51 | 14400: true, 52 | 19200: true, 53 | 28800: true, 54 | 38400: true, 55 | 57600: true, 56 | 76800: true, 57 | 115200: true, 58 | 230400: true, 59 | } 60 | ) 61 | 62 | // IsStandardBaudRate checks whether the specified baud-rate is standard. 63 | // 64 | // Some operating systems may support non-standard baud-rates (OSX) via 65 | // additional IOCTL. 66 | func IsStandardBaudRate(baudRate uint) bool { return StandardBaudRates[baudRate] } 67 | 68 | // OpenOptions is the struct containing all of the options necessary for 69 | // opening a serial port. 70 | type OpenOptions struct { 71 | // The name of the port, e.g. "/dev/tty.usbserial-A8008HlV". 72 | PortName string 73 | 74 | // The baud rate for the port. 75 | BaudRate uint 76 | 77 | // The number of data bits per frame. Legal values are 5, 6, 7, and 8. 78 | DataBits uint 79 | 80 | // The number of stop bits per frame. Legal values are 1 and 2. 81 | StopBits uint 82 | 83 | // The type of parity bits to use for the connection. Currently parity errors 84 | // are simply ignored; that is, bytes are delivered to the user no matter 85 | // whether they were received with a parity error or not. 86 | ParityMode ParityMode 87 | 88 | // Enable RTS/CTS (hardware) flow control. 89 | RTSCTSFlowControl bool 90 | 91 | // An inter-character timeout value, in milliseconds, and a minimum number of 92 | // bytes to block for on each read. A call to Read() that otherwise may block 93 | // waiting for more data will return immediately if the specified amount of 94 | // time elapses between successive bytes received from the device or if the 95 | // minimum number of bytes has been exceeded. 96 | // 97 | // Note that the inter-character timeout value may be rounded to the nearest 98 | // 100 ms on some systems, and that behavior is undefined if calls to Read 99 | // supply a buffer whose length is less than the minimum read size. 100 | // 101 | // Behaviors for various settings for these values are described below. For 102 | // more information, see the discussion of VMIN and VTIME here: 103 | // 104 | // http://www.unixwiz.net/techtips/termios-vmin-vtime.html 105 | // 106 | // InterCharacterTimeout = 0 and MinimumReadSize = 0 (the default): 107 | // This arrangement is not legal; you must explicitly set at least one of 108 | // these fields to a positive number. (If MinimumReadSize is zero then 109 | // InterCharacterTimeout must be at least 100.) 110 | // 111 | // InterCharacterTimeout > 0 and MinimumReadSize = 0 112 | // If data is already available on the read queue, it is transferred to 113 | // the caller's buffer and the Read() call returns immediately. 114 | // Otherwise, the call blocks until some data arrives or the 115 | // InterCharacterTimeout milliseconds elapse from the start of the call. 116 | // Note that in this configuration, InterCharacterTimeout must be at 117 | // least 100 ms. 118 | // 119 | // InterCharacterTimeout > 0 and MinimumReadSize > 0 120 | // Calls to Read() return when at least MinimumReadSize bytes are 121 | // available or when InterCharacterTimeout milliseconds elapse between 122 | // received bytes. The inter-character timer is not started until the 123 | // first byte arrives. 124 | // 125 | // InterCharacterTimeout = 0 and MinimumReadSize > 0 126 | // Calls to Read() return only when at least MinimumReadSize bytes are 127 | // available. The inter-character timer is not used. 128 | // 129 | // For windows usage, these options (termios) do not conform well to the 130 | // windows serial port / comms abstractions. Please see the code in 131 | // open_windows setCommTimeouts function for full documentation. 132 | // Summary: 133 | // Setting MinimumReadSize > 0 will cause the serialPort to block until 134 | // until data is available on the port. 135 | // Setting IntercharacterTimeout > 0 and MinimumReadSize == 0 will cause 136 | // the port to either wait until IntercharacterTimeout wait time is 137 | // exceeded OR there is character data to return from the port. 138 | // 139 | 140 | InterCharacterTimeout uint 141 | MinimumReadSize uint 142 | 143 | // Use to enable RS485 mode -- probably only valid on some Linux platforms 144 | Rs485Enable bool 145 | 146 | // Set to true for logic level high during send 147 | Rs485RtsHighDuringSend bool 148 | 149 | // Set to true for logic level high after send 150 | Rs485RtsHighAfterSend bool 151 | 152 | // set to receive data during sending 153 | Rs485RxDuringTx bool 154 | 155 | // RTS delay before send 156 | Rs485DelayRtsBeforeSend int 157 | 158 | // RTS delay after send 159 | Rs485DelayRtsAfterSend int 160 | } 161 | 162 | // Open creates an io.ReadWriteCloser based on the supplied options struct. 163 | func Open(options OpenOptions) (io.ReadWriteCloser, error) { 164 | // Redirect to the OS-specific function. 165 | return openInternal(options) 166 | } 167 | 168 | // Rounds a float to the nearest integer. 169 | func round(f float64) float64 { 170 | return math.Floor(f + 0.5) 171 | } 172 | -------------------------------------------------------------------------------- /miio_reader.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/AlexxIT/gw3/dict" 6 | "github.com/rs/zerolog/log" 7 | "net" 8 | "os" 9 | "time" 10 | ) 11 | 12 | func miioReader() { 13 | shellPatchApp("miio_agent") 14 | // it is better to kill the miio_agent anyway 15 | shellKillall("miio_agent") 16 | 17 | _ = os.Remove("/tmp/miio_agent.socket") 18 | 19 | sock, err := net.Listen("unixpacket", "/tmp/miio_agent.socket") 20 | if err != nil { 21 | log.Panic().Err(err).Send() 22 | } 23 | 24 | for { 25 | var conn1, conn2 net.Conn 26 | 27 | if conn1, err = sock.Accept(); err != nil { 28 | log.Panic().Err(err).Send() 29 | } 30 | 31 | // original socket from miio_agent 32 | for { 33 | if conn2, err = net.Dial("unixpacket", "/tmp/true_agent.socket"); err == nil { 34 | break 35 | } 36 | time.Sleep(time.Second) 37 | } 38 | 39 | var addr uint8 40 | go miioSocketProxy(conn1, conn2, true, &addr) 41 | go miioSocketProxy(conn2, conn1, false, &addr) 42 | } 43 | } 44 | 45 | const ( 46 | Basic = uint8(0b1) 47 | Bluetooth = uint8(0b10) 48 | Zigbee = uint8(0b100) 49 | HomeKit = uint8(0b1000) 50 | Automation = uint8(0b10000) 51 | Gateway = uint8(0b100000) 52 | ) 53 | 54 | var miioConn = make(map[uint8]miioPair) 55 | 56 | type miioPair struct { 57 | inc net.Conn 58 | out net.Conn 59 | } 60 | 61 | func miioSocketProxy(conn1, conn2 net.Conn, incoming bool, addr *uint8) { 62 | var data *dict.Dict 63 | 64 | var msg string 65 | if incoming { 66 | msg = "miio<-" 67 | } else { 68 | msg = "<-miio" 69 | } 70 | 71 | var b = make([]byte, 1024) 72 | for { 73 | n, err := conn1.Read(b) 74 | if err != nil { 75 | break 76 | } 77 | 78 | log.WithLevel(miioraw).Uint8("addr", *addr).RawJSON("data", b[:n]).Msg(msg) 79 | 80 | if data, err = dict.Unmarshal(b[:n]); err == nil { 81 | switch *addr { 82 | case 0: 83 | if incoming && data.GetString("method", "") == "bind" { 84 | *addr = data.GetUint8("address", 0) 85 | miioConn[*addr] = miioPair{inc: conn1, out: conn2} 86 | 87 | log.Debug().Uint8("addr", *addr).Msg("Open miio connection") 88 | } 89 | case Bluetooth: 90 | if !incoming { 91 | if result := data.GetDict("result"); result != nil { 92 | if result.GetString("operation", "") == "query_dev" { 93 | mac := result.GetString("mac", "") 94 | bindkey := result.GetString("beaconkey", "") 95 | config.SetBindKey(mac, bindkey) 96 | } 97 | } 98 | } 99 | case Zigbee: 100 | if incoming { 101 | switch data.GetString("method", "") { 102 | case "event.gw.heartbeat": 103 | if param := data.GetArrayItem("params", 0); param != nil { 104 | // {"free_mem":5600,"ip":"192.168.1.123","load_avg":"3.18|3.05|2.79|3/95|25132","rssi":65, 105 | // "run_time":43783,"setupcode":"123-45-678","ssid":"WiFi","tz":"GMT3"} 106 | (*param)["action"] = "heartbeat" 107 | gw.updateEvent(param) 108 | } 109 | } 110 | } 111 | case Gateway: 112 | switch data.GetString("method", "") { 113 | case "properties_changed": 114 | if props := data.GetArrayItem("params", 0); props != nil { 115 | miioDecodeGatewayProps(props) 116 | } 117 | case "local.report": 118 | if params := data.GetDict("params"); params != nil { 119 | if button, ok := params.TryGetString("button"); ok { 120 | // click and double_click 121 | data = &dict.Dict{"action": button} 122 | gw.updateEvent(data) 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | if _, err = conn2.Write(b[:n]); err != nil { 130 | break 131 | } 132 | } 133 | 134 | if incoming { 135 | log.Debug().Uint8("addr", *addr).Msg("Close miio connection") 136 | } 137 | 138 | _ = conn2.Close() 139 | if *addr > 0 { 140 | delete(miioConn, *addr) 141 | } 142 | } 143 | 144 | var miioBleQueries = make(map[string]time.Time) 145 | 146 | func miioBleQueryDev(mac string, pdid uint16) { 147 | pair, ok := miioConn[Bluetooth] 148 | if !ok { 149 | log.Debug().Msg("Can't query bindkey") 150 | return 151 | } 152 | 153 | var ts time.Time 154 | 155 | // not more than once in 60 minutes 156 | now := time.Now() 157 | if ts, ok = miioBleQueries[mac]; ok && now.Before(ts) { 158 | return 159 | } 160 | miioBleQueries[mac] = now.Add(time.Hour) 161 | 162 | log.Debug().Str("mac", mac).Msg("Query bindkey") 163 | 164 | id := uint32(time.Now().Nanosecond()) & 0xFFFFFF 165 | p := []byte(fmt.Sprintf( 166 | `{"id":%d,"method":"_sync.ble_query_dev","params":{"mac":"%s","pdid":%d}}`, 167 | id, mac, pdid, 168 | )) 169 | 170 | if _, err := pair.out.Write(p); err != nil { 171 | log.Warn().Err(err).Send() 172 | } 173 | } 174 | 175 | // name: {siid, piid, value} 176 | var miioAlarmStates = map[string][3]uint8{ 177 | "disarmed": {3, 1, 0}, 178 | "armed_home": {3, 1, 1}, 179 | "armed_away": {3, 1, 2}, 180 | "armed_night": {3, 1, 3}, 181 | "": {3, 22, 0}, 182 | "triggered": {3, 22, 1}, 183 | } 184 | 185 | func miioEncodeGatewayProps(state string) { 186 | pair, ok := miioConn[Gateway] 187 | if !ok { 188 | log.Debug().Msg("Can't set gateway props") 189 | return 190 | } 191 | 192 | if v, ok := miioAlarmStates[state]; ok { 193 | id := uint32(time.Now().Nanosecond()) & 0xFFFFFF 194 | p := []byte(fmt.Sprintf( 195 | `{"from":"4","id":%d,"method":"set_properties","params":[{"did":"%s","piid":%d,"siid":%d,"value":%d}]}`, 196 | id, gw.Miio.Did, v[1], v[0], v[2], 197 | )) 198 | 199 | if _, err := pair.inc.Write(p); err != nil { 200 | log.Warn().Err(err).Send() 201 | } 202 | } 203 | } 204 | 205 | func miioDecodeGatewayProps(props *dict.Dict) { 206 | siid := props.GetUint8("siid", 0) 207 | piid := props.GetUint8("piid", 0) 208 | value := props.GetUint8("value", 255) 209 | for k, v := range miioAlarmStates { 210 | if v[0] == siid && v[1] == piid && v[2] == value { 211 | gw.updateAlarmState(k) 212 | return 213 | } 214 | } 215 | } 216 | 217 | func miioEncodeGatewayBuzzer(duration uint64, volume uint8) { 218 | pair, ok := miioConn[Basic] 219 | if !ok { 220 | log.Debug().Msg("Can't run buzzer") 221 | return 222 | } 223 | 224 | var b []byte 225 | id := uint32(time.Now().Nanosecond()) & 0xFFFFFF 226 | 227 | if volume > 0 { 228 | b = []byte(fmt.Sprintf( 229 | `{"from":32,"id":%d,"method":"local.status","params":"start_alarm,%d,%d"}`, 230 | id, duration, volume, 231 | )) 232 | } else { 233 | b = []byte(fmt.Sprintf( 234 | `{"from":32,"id":%d,"method":"local.status","params":"stop_alarm"}`, id, 235 | )) 236 | } 237 | 238 | if _, err := pair.inc.Write(b); err != nil { 239 | log.Warn().Err(err).Send() 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /crypt/crypt.go: -------------------------------------------------------------------------------- 1 | // CCM Mode, defined in 2 | // NIST Special Publication SP 800-38C. 3 | 4 | package crypt 5 | 6 | import ( 7 | "bytes" 8 | "crypto/cipher" 9 | "errors" 10 | ) 11 | 12 | // CBC-MAC implementation 13 | type mac struct { 14 | ci []byte 15 | p int 16 | c cipher.Block 17 | } 18 | 19 | func newMAC(c cipher.Block) *mac { 20 | return &mac{ 21 | c: c, 22 | ci: make([]byte, c.BlockSize()), 23 | } 24 | } 25 | 26 | func (m *mac) Reset() { 27 | for i := range m.ci { 28 | m.ci[i] = 0 29 | } 30 | m.p = 0 31 | } 32 | 33 | func (m *mac) Write(p []byte) (n int, err error) { 34 | for _, c := range p { 35 | if m.p >= len(m.ci) { 36 | m.c.Encrypt(m.ci, m.ci) 37 | m.p = 0 38 | } 39 | m.ci[m.p] ^= c 40 | m.p++ 41 | } 42 | return len(p), nil 43 | } 44 | 45 | // PadZero emulates zero byte padding. 46 | func (m *mac) PadZero() { 47 | if m.p != 0 { 48 | m.c.Encrypt(m.ci, m.ci) 49 | m.p = 0 50 | } 51 | } 52 | 53 | func (m *mac) Sum(in []byte) []byte { 54 | if m.p != 0 { 55 | m.c.Encrypt(m.ci, m.ci) 56 | m.p = 0 57 | } 58 | return append(in, m.ci...) 59 | } 60 | 61 | func (m *mac) Size() int { return len(m.ci) } 62 | 63 | func (m *mac) BlockSize() int { return 16 } 64 | 65 | type ccm struct { 66 | c cipher.Block 67 | mac *mac 68 | nonceSize int 69 | tagSize int 70 | } 71 | 72 | // NewCCMWithNonceAndTagSizes returns the given 128-bit, block cipher wrapped in Counter with CBC-MAC Mode, which accepts nonces of the given length. 73 | // the formatting of this function is defined in SP800-38C, Appendix A. 74 | // Each arguments have own valid range: 75 | // nonceSize should be one of the {7, 8, 9, 10, 11, 12, 13}. 76 | // tagSize should be one of the {4, 6, 8, 10, 12, 14, 16}. 77 | // Otherwise, it panics. 78 | // The maximum payload size is defined as 1< 0 { 194 | B[0] |= 1 << 6 // Adata 195 | 196 | ccm.mac.Write(B) 197 | 198 | if len(data) < (1<<15 - 1<<7) { 199 | putUvarint(B[:2], uint64(len(data))) 200 | 201 | ccm.mac.Write(B[:2]) 202 | } else if len(data) <= 1<<31-1 { 203 | B[0] = 0xff 204 | B[1] = 0xfe 205 | putUvarint(B[2:6], uint64(len(data))) 206 | 207 | ccm.mac.Write(B[:6]) 208 | } else { 209 | B[0] = 0xff 210 | B[1] = 0xff 211 | putUvarint(B[2:10], uint64(len(data))) 212 | 213 | ccm.mac.Write(B[:10]) 214 | } 215 | ccm.mac.Write(data) 216 | ccm.mac.PadZero() 217 | } else { 218 | ccm.mac.Write(B) 219 | } 220 | 221 | ccm.mac.Write(plaintext) 222 | ccm.mac.PadZero() 223 | 224 | return ccm.mac.Sum(nil) 225 | } 226 | 227 | func maxUvarint(n int) uint64 { 228 | return 1<> uint(8*(len(bs)-1-i))) 235 | } 236 | } 237 | 238 | // defined in crypto/cipher/gcm.go 239 | func sliceForAppend(in []byte, n int) (head, tail []byte) { 240 | if total := len(in) + n; cap(in) >= total { 241 | head = in[:total] 242 | } else { 243 | head = make([]byte, total) 244 | copy(head, in) 245 | } 246 | tail = head[len(in):] 247 | return 248 | } 249 | 250 | // defined in crypto/cipher/xor.go 251 | func xorBytes(dst, a, b []byte) int { 252 | n := len(a) 253 | if len(b) < n { 254 | n = len(b) 255 | } 256 | for i := 0; i < n; i++ { 257 | dst[i] = a[i] ^ b[i] 258 | } 259 | return n 260 | } 261 | -------------------------------------------------------------------------------- /btchip_reader.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/AlexxIT/gw3/bglib" 6 | "github.com/AlexxIT/gw3/gap" 7 | "github.com/AlexxIT/gw3/serial" 8 | "github.com/rs/zerolog/log" 9 | "io" 10 | "time" 11 | ) 12 | 13 | var btchip io.ReadWriteCloser 14 | 15 | // btchipInit open serial connection to /dev/ttyS1 16 | func btchipInit() { 17 | var err error 18 | btchip, err = serial.Open(serial.OpenOptions{ 19 | PortName: "/dev/ttyS1", 20 | BaudRate: 115200, 21 | DataBits: 8, 22 | StopBits: 1, 23 | MinimumReadSize: 1, 24 | //RTSCTSFlowControl: true, 25 | }) 26 | if err != nil { 27 | log.Panic().Err(err).Send() 28 | } 29 | } 30 | 31 | const ( 32 | StateNone = iota 33 | StateReset 34 | StateSetup 35 | StateDiscovery 36 | ) 37 | 38 | // btchipReader loops reading data from BT chip 39 | func btchipReader() { 40 | go btchipWriter() 41 | 42 | var p = make([]byte, 260) // max payload size + 4 43 | 44 | var skipBuf = make([]byte, 256) 45 | var skipN int 46 | 47 | state := StateNone 48 | gw.updateState("setup") 49 | 50 | // We wait a minute for the start of discovery mode. After that any data from the chip updates the timer. 51 | var discoveryTimer *time.Timer 52 | discoveryTimer = time.AfterFunc(time.Minute, func() { 53 | state = StateNone 54 | gw.updateState("setup") 55 | 56 | log.Info().Str("state", "restart").Msg("Bluetooth state") 57 | shellKillall("silabs_ncp_bt") 58 | 59 | discoveryTimer.Reset(config.discoveryDelay) 60 | }) 61 | 62 | // bglib reader will return full command/event or return only 1 byte for wrong response bytes 63 | // fw v1.4.6_0012 returns 0x937162AD at start of each command/event 64 | // fw v1.5.0_0026 returns 0xC0 at start and at end of each command/event 65 | reader := bglib.NewReader(btchip) 66 | for { 67 | n, err := reader.Read(p) 68 | if n == 0 { 69 | log.Debug().Err(err).Msg("btchip.Read") 70 | continue 71 | } 72 | 73 | if err == nil { 74 | // don't care if skip len lower than 5 bytes 75 | if skipN > 4 { 76 | log.WithLevel(btskip).Hex("data", skipBuf[:skipN]).Msg("Skip wrong bytes") 77 | } 78 | skipN = 0 79 | 80 | header, _ := bglib.DecodeResponse(p, n) 81 | 82 | // process only logs 83 | switch header { 84 | case bglib.Evt_le_gap_extended_scan_response: 85 | log.WithLevel(btgap).Hex("data", p[:n]).Msg("<-btgap") 86 | default: 87 | log.WithLevel(btraw).Hex("data", p[:n]).Int("q", len(btchipQueue)).Msg("<-btraw") 88 | } 89 | 90 | // process data 91 | switch header { 92 | case bglib.Cmd_system_get_bt_address: 93 | // btapp sends command two times 94 | if state != StateSetup { 95 | state = StateSetup 96 | log.Info().Str("state", "setup").Msg("Bluetooth state") 97 | } 98 | case bglib.Cmd_le_gap_set_discovery_extended_scan_response: 99 | log.Debug().Msg("<-cmd_le_gap_set_discovery_extended_scan_response") 100 | // no need to forward response to this command 101 | n = 0 102 | case bglib.Cmd_le_gap_start_discovery: 103 | shellPatchTimerStart() 104 | state = StateDiscovery 105 | gw.updateState("discovery") 106 | log.Info().Str("state", "discovery").Msg("Bluetooth state") 107 | case bglib.Evt_system_boot: 108 | shellPatchTimerStop() 109 | state = StateReset 110 | gw.updateState("setup") 111 | 112 | if !bglib.IsResetCmd(btchipReq) { 113 | // silabs_ncp_bt reboot chip at startup using GPIO 114 | log.Info().Str("state", "hardreset").Msg("Bluetooth state") 115 | // clear queue on hardreset 116 | btchipQueueClear() 117 | // no need to forward event in this case 118 | continue 119 | } 120 | log.Info().Str("state", "softreset").Msg("Bluetooth state") 121 | 122 | // clear resp only after softreset 123 | btchipRespClear() 124 | case bglib.Evt_le_gap_extended_scan_response: 125 | n = btchipProcessExtResponse(p[:n]) 126 | } 127 | 128 | if p[0] == 0x20 { 129 | btchipRespClear() 130 | } 131 | 132 | if state == StateDiscovery { 133 | // any message in discovery state update btapp watchdog timer 134 | discoveryTimer.Reset(config.discoveryDelay) 135 | } 136 | } else if n > 1 { 137 | log.WithLevel(btskip).Hex("data", p[:n]).Msg("Skip wrong bytes") 138 | } else if skipN < 256 { 139 | skipBuf[skipN] = p[0] 140 | skipN++ 141 | } 142 | 143 | _, err = btapp.Write(p[:n]) 144 | if err != nil { 145 | log.Panic().Err(err).Send() 146 | } 147 | } 148 | } 149 | 150 | // raw commands chan to BT chip, because we should send new command 151 | // only after receive response from previous command 152 | var btchipQueue = make(chan []byte, 100) 153 | 154 | // raw response chan from BT chip, receive only commands responses 155 | var btchipResp = make(chan bool) 156 | 157 | func btchipQueueAdd(p []byte) { 158 | btchipQueue <- p 159 | } 160 | 161 | func btchipQueueClear() { 162 | for len(btchipQueue) > 0 { 163 | select { 164 | case <-btchipQueue: 165 | default: 166 | } 167 | } 168 | } 169 | 170 | // unblock btchipResp chan even if no waiters 171 | func btchipRespClear() { 172 | btchipReq = nil 173 | 174 | select { 175 | case btchipResp <- true: 176 | default: 177 | } 178 | } 179 | 180 | var btchipReq []byte 181 | 182 | func btchipWriter() { 183 | for btchipReq = range btchipQueue { 184 | log.WithLevel(btraw).Hex("data", btchipReq).Int("q", len(btchipQueue)).Msg("btraw<-") 185 | 186 | if _, err := btchip.Write(btchipReq); err != nil { 187 | log.Panic().Err(err).Send() 188 | } 189 | 190 | //log.Debug().Msg("wait") 191 | <-btchipResp 192 | //log.Debug().Msg("continue") 193 | } 194 | } 195 | 196 | var btchipRepeatFilter = RepeatFilter{cache: make(map[string]time.Time), clear: time.Now()} 197 | 198 | // btchipProcessExtResponse unpack GAP extended scan response from BT chip 199 | // skips same data for 5 seconds 200 | // conveverts data to simple scan response because BT app can process only it 201 | func btchipProcessExtResponse(data []byte) int { 202 | // save rssi before clear 203 | rssi := data[17] 204 | 205 | // clear rssi and channel before repeatFileter check 206 | data[17] = 0 207 | data[18] = 0 208 | 209 | // check if message in repeatFileter 210 | if btchipRepeatFilter.Test(string(data)) { 211 | return 0 212 | } 213 | 214 | // restore rssi 215 | data[17] = rssi 216 | 217 | n := bglib.ConvertExtendedToLegacy(data) 218 | msg := gap.ParseScanResponse(data[:n]) 219 | 220 | var payload gap.Map 221 | 222 | switch msg.ServiceUUID { 223 | case 0x181A: 224 | if payload = gap.ParseATC1441(msg.Raw[0x16][2:]); payload != nil { 225 | btchipProcessBLE(msg.MAC, "atc1441", payload) 226 | } 227 | 228 | case 0x181B: 229 | if payload = gap.ParseMiScalesV2(msg.Raw[0x16][2:]); payload != nil { 230 | btchipProcessBLE(msg.MAC, "miscales2", payload) 231 | } 232 | 233 | case 0x181D: 234 | if payload = gap.ParseMiScalesV1(msg.Raw[0x16][2:]); payload != nil { 235 | btchipProcessBLE(msg.MAC, "miscales", payload) 236 | } 237 | 238 | case 0xFE95: 239 | mibeacon, useful := gap.ParseMiBeacon(msg.Raw[0x16][2:], config.GetBindkey) 240 | //log.Debug().Uint8("useful", useful).Msgf("%+v", mibeacon) 241 | if useful > 0 { 242 | if useful == 1 { 243 | if mibeacon.Comment == "wrong enc key" { 244 | log.Warn().Hex("data", data[:n]).Msg("Wrong MiBeacon key") 245 | } 246 | // is encrypted 247 | miioBleQueryDev(mibeacon.Mac, mibeacon.Pdid) 248 | } 249 | advType := fmt.Sprintf("mi:%d", mibeacon.Pdid) 250 | btchipProcessBLE(msg.MAC, advType, mibeacon.Decode()) 251 | } 252 | } 253 | 254 | switch msg.CompanyID { 255 | case 0x004C: // iBeacon 256 | if payload = gap.ParseIBeacon(msg.Raw[0xFF][2:]); payload != nil { 257 | id := fmt.Sprintf("%s-%d-%d", payload["uuid"], payload["major"], payload["minor"]) 258 | btchipProcessBLETracker(id, "ibeacon", msg.RSSI) 259 | } 260 | case 0x00D2: // Nut 261 | btchipProcessBLETracker(msg.MAC, "nut", msg.RSSI) 262 | case 0x0157: // MiBand or Amazfit Watch 263 | // don't know how to parse payload, but can be used as tracker 264 | btchipProcessBLETracker(msg.MAC, "miband", msg.RSSI) 265 | } 266 | 267 | return n 268 | } 269 | 270 | func btchipProcessBLE(mac string, advType string, data gap.Map) { 271 | device, ok := devices[mac] 272 | if !ok { 273 | device = newBLEDevice(mac, advType) 274 | } 275 | if data != nil { 276 | device.(*BLEDevice).updateState(data) 277 | } 278 | } 279 | 280 | var btchipTrackers = make(map[string]uint8) 281 | 282 | func btchipProcessBLETracker(mac string, advType string, rssi int8) { 283 | // detects tracker only after 10 events 284 | if _, ok := btchipTrackers[mac]; !ok { 285 | btchipTrackers[mac] = 1 286 | return 287 | } else if btchipTrackers[mac] < 10 { 288 | btchipTrackers[mac]++ 289 | return 290 | } 291 | 292 | device, ok := devices[mac] 293 | if !ok { 294 | device = newBLEDevice(mac, advType) 295 | } 296 | 297 | data := gap.Map{"action": "tracker", "rssi": rssi, "tracker": gw.WiFi.MAC} 298 | device.(*BLEDevice).updateState(data) 299 | } 300 | 301 | type RepeatFilter struct { 302 | cache map[string]time.Time 303 | clear time.Time 304 | } 305 | 306 | func (r *RepeatFilter) Test(key string) bool { 307 | now := time.Now() 308 | if r.clear.After(now) { 309 | for k, v := range r.cache { 310 | if now.After(v) { 311 | delete(r.cache, k) 312 | } 313 | } 314 | // clear cache once per minute 315 | r.clear = now.Add(time.Minute) 316 | } 317 | 318 | if ts, ok := r.cache[key]; ok && now.Before(ts) { 319 | return true 320 | } 321 | 322 | // put key in cache on 5 seconds 323 | r.cache[key] = now.Add(time.Second * 5) 324 | 325 | return false 326 | } 327 | -------------------------------------------------------------------------------- /serial/open_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Aaron Jacobs. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package serial 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "os" 21 | "sync" 22 | "syscall" 23 | "unsafe" 24 | ) 25 | 26 | type serialPort struct { 27 | f *os.File 28 | fd syscall.Handle 29 | rl sync.Mutex 30 | wl sync.Mutex 31 | ro *syscall.Overlapped 32 | wo *syscall.Overlapped 33 | } 34 | 35 | type structDCB struct { 36 | DCBlength, BaudRate uint32 37 | flags [4]byte 38 | wReserved, XonLim, XoffLim uint16 39 | ByteSize, Parity, StopBits byte 40 | XonChar, XoffChar, ErrorChar, EofChar, EvtChar byte 41 | wReserved1 uint16 42 | } 43 | 44 | type structTimeouts struct { 45 | ReadIntervalTimeout uint32 46 | ReadTotalTimeoutMultiplier uint32 47 | ReadTotalTimeoutConstant uint32 48 | WriteTotalTimeoutMultiplier uint32 49 | WriteTotalTimeoutConstant uint32 50 | } 51 | 52 | func openInternal(options OpenOptions) (io.ReadWriteCloser, error) { 53 | if len(options.PortName) > 0 && options.PortName[0] != '\\' { 54 | options.PortName = "\\\\.\\" + options.PortName 55 | } 56 | 57 | h, err := syscall.CreateFile(syscall.StringToUTF16Ptr(options.PortName), 58 | syscall.GENERIC_READ|syscall.GENERIC_WRITE, 59 | 0, 60 | nil, 61 | syscall.OPEN_EXISTING, 62 | syscall.FILE_ATTRIBUTE_NORMAL|syscall.FILE_FLAG_OVERLAPPED, 63 | 0) 64 | if err != nil { 65 | return nil, err 66 | } 67 | f := os.NewFile(uintptr(h), options.PortName) 68 | defer func() { 69 | if err != nil { 70 | f.Close() 71 | } 72 | }() 73 | 74 | if err = setCommState(h, options); err != nil { 75 | return nil, err 76 | } 77 | if err = setupComm(h, 64, 64); err != nil { 78 | return nil, err 79 | } 80 | if err = setCommTimeouts(h, options); err != nil { 81 | return nil, err 82 | } 83 | if err = setCommMask(h); err != nil { 84 | return nil, err 85 | } 86 | 87 | ro, err := newOverlapped() 88 | if err != nil { 89 | return nil, err 90 | } 91 | wo, err := newOverlapped() 92 | if err != nil { 93 | return nil, err 94 | } 95 | port := new(serialPort) 96 | port.f = f 97 | port.fd = h 98 | port.ro = ro 99 | port.wo = wo 100 | 101 | return port, nil 102 | } 103 | 104 | func (p *serialPort) Close() error { 105 | return p.f.Close() 106 | } 107 | 108 | func (p *serialPort) Write(buf []byte) (int, error) { 109 | p.wl.Lock() 110 | defer p.wl.Unlock() 111 | 112 | if err := resetEvent(p.wo.HEvent); err != nil { 113 | return 0, err 114 | } 115 | var n uint32 116 | err := syscall.WriteFile(p.fd, buf, &n, p.wo) 117 | if err != nil && err != syscall.ERROR_IO_PENDING { 118 | return int(n), err 119 | } 120 | return getOverlappedResult(p.fd, p.wo) 121 | } 122 | 123 | func (p *serialPort) Read(buf []byte) (int, error) { 124 | if p == nil || p.f == nil { 125 | return 0, fmt.Errorf("Invalid port on read %v %v", p, p.f) 126 | } 127 | 128 | p.rl.Lock() 129 | defer p.rl.Unlock() 130 | 131 | if err := resetEvent(p.ro.HEvent); err != nil { 132 | return 0, err 133 | } 134 | var done uint32 135 | err := syscall.ReadFile(p.fd, buf, &done, p.ro) 136 | if err != nil && err != syscall.ERROR_IO_PENDING { 137 | return int(done), err 138 | } 139 | return getOverlappedResult(p.fd, p.ro) 140 | } 141 | 142 | var ( 143 | nSetCommState, 144 | nSetCommTimeouts, 145 | nSetCommMask, 146 | nSetupComm, 147 | nGetOverlappedResult, 148 | nCreateEvent, 149 | nResetEvent uintptr 150 | ) 151 | 152 | func init() { 153 | k32, err := syscall.LoadLibrary("kernel32.dll") 154 | if err != nil { 155 | panic("LoadLibrary " + err.Error()) 156 | } 157 | defer syscall.FreeLibrary(k32) 158 | 159 | nSetCommState = getProcAddr(k32, "SetCommState") 160 | nSetCommTimeouts = getProcAddr(k32, "SetCommTimeouts") 161 | nSetCommMask = getProcAddr(k32, "SetCommMask") 162 | nSetupComm = getProcAddr(k32, "SetupComm") 163 | nGetOverlappedResult = getProcAddr(k32, "GetOverlappedResult") 164 | nCreateEvent = getProcAddr(k32, "CreateEventW") 165 | nResetEvent = getProcAddr(k32, "ResetEvent") 166 | } 167 | 168 | func getProcAddr(lib syscall.Handle, name string) uintptr { 169 | addr, err := syscall.GetProcAddress(lib, name) 170 | if err != nil { 171 | panic(name + " " + err.Error()) 172 | } 173 | return addr 174 | } 175 | 176 | func setCommState(h syscall.Handle, options OpenOptions) error { 177 | var params structDCB 178 | params.DCBlength = uint32(unsafe.Sizeof(params)) 179 | 180 | params.flags[0] = 0x01 // fBinary 181 | params.flags[0] |= 0x10 // Assert DSR 182 | 183 | if options.ParityMode != PARITY_NONE { 184 | params.flags[0] |= 0x03 // fParity 185 | params.Parity = byte(options.ParityMode) 186 | } 187 | 188 | if options.StopBits == 1 { 189 | params.StopBits = 0 190 | } else if options.StopBits == 2 { 191 | params.StopBits = 2 192 | } 193 | 194 | params.BaudRate = uint32(options.BaudRate) 195 | params.ByteSize = byte(options.DataBits) 196 | 197 | if options.RTSCTSFlowControl { 198 | params.flags[0] |= 0x04 // fOutxCtsFlow = 0x1 199 | params.flags[1] |= 0x20 // fRtsControl = RTS_CONTROL_HANDSHAKE (0x2) 200 | } 201 | 202 | r, _, err := syscall.Syscall(nSetCommState, 2, uintptr(h), uintptr(unsafe.Pointer(¶ms)), 0) 203 | if r == 0 { 204 | return err 205 | } 206 | return nil 207 | } 208 | 209 | func setCommTimeouts(h syscall.Handle, options OpenOptions) error { 210 | var timeouts structTimeouts 211 | const MAXDWORD = 1<<32 - 1 212 | timeoutConstant := uint32(round(float64(options.InterCharacterTimeout) / 100.0)) 213 | readIntervalTimeout := uint32(options.MinimumReadSize) 214 | 215 | if timeoutConstant > 0 && readIntervalTimeout == 0 { 216 | //Assume we're setting for non blocking IO. 217 | timeouts.ReadIntervalTimeout = MAXDWORD 218 | timeouts.ReadTotalTimeoutMultiplier = MAXDWORD 219 | timeouts.ReadTotalTimeoutConstant = timeoutConstant 220 | } else if readIntervalTimeout > 0 { 221 | // Assume we want to block and wait for input. 222 | timeouts.ReadIntervalTimeout = readIntervalTimeout 223 | timeouts.ReadTotalTimeoutMultiplier = 1 224 | timeouts.ReadTotalTimeoutConstant = 1 225 | } else { 226 | // No idea what we intended, use defaults 227 | // default config does what it did before. 228 | timeouts.ReadIntervalTimeout = MAXDWORD 229 | timeouts.ReadTotalTimeoutMultiplier = MAXDWORD 230 | timeouts.ReadTotalTimeoutConstant = MAXDWORD - 1 231 | } 232 | 233 | /* 234 | Empirical testing has shown that to have non-blocking IO we need to set: 235 | ReadTotalTimeoutConstant > 0 and 236 | ReadTotalTimeoutMultiplier = MAXDWORD and 237 | ReadIntervalTimeout = MAXDWORD 238 | 239 | The documentation states that ReadIntervalTimeout is set in MS but 240 | empirical investigation determines that it seems to interpret in units 241 | of 100ms. 242 | 243 | If InterCharacterTimeout is set at all it seems that the port will block 244 | indefinitly until a character is received. Not all circumstances have been 245 | tested. The input of an expert would be appreciated. 246 | 247 | From http://msdn.microsoft.com/en-us/library/aa363190(v=VS.85).aspx 248 | 249 | For blocking I/O see below: 250 | 251 | Remarks: 252 | 253 | If an application sets ReadIntervalTimeout and 254 | ReadTotalTimeoutMultiplier to MAXDWORD and sets 255 | ReadTotalTimeoutConstant to a value greater than zero and 256 | less than MAXDWORD, one of the following occurs when the 257 | ReadFile function is called: 258 | 259 | If there are any bytes in the input buffer, ReadFile returns 260 | immediately with the bytes in the buffer. 261 | 262 | If there are no bytes in the input buffer, ReadFile waits 263 | until a byte arrives and then returns immediately. 264 | 265 | If no bytes arrive within the time specified by 266 | ReadTotalTimeoutConstant, ReadFile times out. 267 | */ 268 | 269 | r, _, err := syscall.Syscall(nSetCommTimeouts, 2, uintptr(h), uintptr(unsafe.Pointer(&timeouts)), 0) 270 | if r == 0 { 271 | return err 272 | } 273 | return nil 274 | } 275 | 276 | func setupComm(h syscall.Handle, in, out int) error { 277 | r, _, err := syscall.Syscall(nSetupComm, 3, uintptr(h), uintptr(in), uintptr(out)) 278 | if r == 0 { 279 | return err 280 | } 281 | return nil 282 | } 283 | 284 | func setCommMask(h syscall.Handle) error { 285 | const EV_RXCHAR = 0x0001 286 | r, _, err := syscall.Syscall(nSetCommMask, 2, uintptr(h), EV_RXCHAR, 0) 287 | if r == 0 { 288 | return err 289 | } 290 | return nil 291 | } 292 | 293 | func resetEvent(h syscall.Handle) error { 294 | r, _, err := syscall.Syscall(nResetEvent, 1, uintptr(h), 0, 0) 295 | if r == 0 { 296 | return err 297 | } 298 | return nil 299 | } 300 | 301 | func newOverlapped() (*syscall.Overlapped, error) { 302 | var overlapped syscall.Overlapped 303 | r, _, err := syscall.Syscall6(nCreateEvent, 4, 0, 1, 0, 0, 0, 0) 304 | if r == 0 { 305 | return nil, err 306 | } 307 | overlapped.HEvent = syscall.Handle(r) 308 | return &overlapped, nil 309 | } 310 | 311 | func getOverlappedResult(h syscall.Handle, overlapped *syscall.Overlapped) (int, error) { 312 | var n int 313 | r, _, err := syscall.Syscall6(nGetOverlappedResult, 4, 314 | uintptr(h), 315 | uintptr(unsafe.Pointer(overlapped)), 316 | uintptr(unsafe.Pointer(&n)), 1, 0, 0) 317 | if r == 0 { 318 | return n, err 319 | } 320 | 321 | return n, nil 322 | } 323 | -------------------------------------------------------------------------------- /bglib/bglib.go: -------------------------------------------------------------------------------- 1 | package bglib 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "github.com/rs/zerolog/log" 9 | "io" 10 | ) 11 | 12 | var WrongRead = errors.New("") 13 | 14 | const Cmd_system_reset = 0x2000_0101 15 | const Cmd_system_get_bt_address = 0x2000_0103 16 | const Cmd_le_gap_set_discovery_timing = 0x2000_0316 17 | const Cmd_le_gap_start_discovery = 0x2000_0318 18 | const Cmd_le_gap_set_discovery_extended_scan_response = 0x2000_031C 19 | const Cmd_mesh_node_set_ivrecovery_mode = 0x2000_1406 20 | 21 | const Evt_system_boot = 0xA000_0100 22 | const Evt_le_gap_adv_timeout = 0xA000_0301 23 | const Evt_le_gap_extended_scan_response = 0xA000_0304 24 | 25 | type ChipReader struct { 26 | r io.Reader 27 | } 28 | 29 | func NewReader(r io.Reader) *ChipReader { 30 | return &ChipReader{r} 31 | } 32 | 33 | func (r *ChipReader) Read(p []byte) (int, error) { 34 | buf := make([]byte, 260) // max payload size + 4 35 | n := 0 36 | chunk := make([]byte, 1) 37 | expectedLen := 0 38 | 39 | isGap := false 40 | 41 | for { 42 | if _, err := r.r.Read(chunk); err != nil { 43 | return 0, err 44 | } 45 | 46 | // type byte = 0xA0 or 0x20 47 | if n == 0 && (chunk[0]&0x7F != 0x20) { 48 | return copy(p, chunk[:1]), WrongRead 49 | } 50 | 51 | // new firmware has a bug with DB/DC or DB/DD bytes in payload in place of only one byte 52 | // we should jump over this byte 53 | // a026030400______50ec5000ff0100ff7fdbdc26000014020106101695fe9055eb0601______50ec500e00 54 | // ^^^^ 55 | // a028030403______50ec5000ff0100ff7fb327000016152a6935020c0fefdbdd214e1e7768c729201c86fd36c7 56 | // ^^^^ 57 | // a028030403______50ec5000ff0100ff7fbb27000016152a69760eef45d6d8e271866b11dbddcf03c8ee530d01 58 | // ^^^^ 59 | // a025030400______38c1a400ff0100ff7fd12700001312161a18______38c1a440086714060b45dbdc0f c0c0 60 | // ^^^^ last byte 61 | if isGap && buf[n-1] == 0xDB && (chunk[0] == 0xDC || chunk[0] == 0xDD) { 62 | continue 63 | } 64 | 65 | buf[n] = chunk[0] 66 | n++ 67 | 68 | switch n { 69 | case 2: 70 | // length should be greater than 0 71 | if buf[1] == 0 { 72 | return copy(p, buf[:2]), WrongRead 73 | } 74 | // cmdType + payloadLen + classID + commandID + payload 75 | expectedLen = 4 + int(buf[1]) 76 | 77 | case 4: 78 | // evt_le_gap_extended_scan_response (most frequent event) 79 | isGap = buf[0] == 0xA0 && buf[2] == 0x03 && buf[3] == 0x04 80 | 81 | // minimum length byte = 4 + 0x12 82 | if isGap && expectedLen < 22 { 83 | return copy(p, buf[:4]), WrongRead 84 | } 85 | 86 | case expectedLen: 87 | // byte 21 = size of advertising data 88 | if isGap && buf[21] != byte(n)-22 { 89 | return copy(p, buf[:expectedLen]), WrongRead 90 | } 91 | 92 | return copy(p, buf[:expectedLen]), nil 93 | 94 | case 16: 95 | // sometimes data has a bug for evt_le_gap_extended_scan_response 96 | // byte 15 = adv_sid always should be 0xFF (don't know why) 97 | if isGap && buf[15] != 0xFF { 98 | return copy(p, buf[:16]), WrongRead 99 | } 100 | 101 | case 260: 102 | // this shouldn't happen 103 | log.Panic().Hex("data", buf[:260]).Int("len", expectedLen).Send() 104 | } 105 | } 106 | } 107 | 108 | func IsResetCmd(p []byte) bool { 109 | return bytes.Compare(p, []byte{0x20, 1, 1, 1, 0}) == 0 110 | } 111 | 112 | type Map map[string]interface{} 113 | 114 | func DecodeResponse(p []byte, n int) (uint32, Map) { 115 | header := uint32(p[0])<<24 | uint32(p[2])<<8 | uint32(p[3]) 116 | switch header { 117 | case Cmd_system_get_bt_address: 118 | if n == 10 && p[1] == 0x06 { 119 | return header, nil 120 | } 121 | case Cmd_le_gap_set_discovery_extended_scan_response: 122 | if n == 6 && p[1] == 0x02 { 123 | return header, nil 124 | } 125 | case Cmd_le_gap_start_discovery: 126 | if n == 6 && p[1] == 0x02 { 127 | return header, nil 128 | } 129 | case Evt_system_boot: 130 | if n == 22 && p[1] == 0x12 { 131 | return header, nil 132 | } 133 | case Evt_le_gap_extended_scan_response: 134 | if n >= 22 && p[1] >= 0x12 && p[21] == byte(n)-22 { 135 | return header, nil 136 | } 137 | } 138 | return 0, nil 139 | } 140 | 141 | func PatchGapDiscoveryTiming(p []byte, scanInterval uint16, scanWindow uint16) { 142 | // cmd_le_gap_set_discovery_timing 143 | binary.LittleEndian.PutUint16(p[5:], scanInterval) 144 | binary.LittleEndian.PutUint16(p[7:], scanWindow) 145 | } 146 | 147 | func EncodeGapExtendedScan(enabled uint8) []byte { 148 | // cmd_le_gap_set_discovery_extended_scan_response 149 | return []byte{0x20, 0x01, 0x03, 0x1C, enabled} 150 | } 151 | 152 | type ScanResponse struct { 153 | RSSI int8 154 | PacketType byte 155 | Addr []byte 156 | AddrType byte 157 | Bounding byte 158 | DataLen byte 159 | Data []byte 160 | } 161 | 162 | func ConvertExtendedToLegacy(extended []byte) int { 163 | // legacy payload is 7 bytes less 164 | legacy := make([]byte, 0, len(extended)-7) 165 | legacy = append(legacy, 0xA0, extended[2]-7, 0x03, 0x00) 166 | // rssi[17] 167 | legacy = append(legacy, extended[17]) 168 | // packet_type[4], addr[5:11], addr_type[11], bonding[12] 169 | legacy = append(legacy, extended[4:13]...) 170 | // data_len[21], data 171 | legacy = append(legacy, extended[21:]...) 172 | return copy(extended, legacy) 173 | } 174 | 175 | func ParseVersion(b []byte) string { 176 | maj := binary.LittleEndian.Uint16(b[4:]) 177 | min := binary.LittleEndian.Uint16(b[6:]) 178 | pat := binary.LittleEndian.Uint16(b[8:]) 179 | return fmt.Sprintf("%d.%d.%d", maj, min, pat) 180 | } 181 | 182 | func ParseProvInit(b []byte) (addr uint16, ivi uint32) { 183 | return binary.LittleEndian.Uint16(b[5:]), binary.LittleEndian.Uint32(b[7:]) 184 | } 185 | 186 | func ParseDeviceInfo(b []byte) (uuid []byte, address uint16, elements uint8) { 187 | return b[4:20], binary.LittleEndian.Uint16(b[20:]), b[22] 188 | } 189 | 190 | //func ParseMeshReceive(b []byte) (address uint16, payload []byte) { 191 | // l := 20 192 | // return binary.LittleEndian.Uint16(b[10:]), b[l:] 193 | //} 194 | 195 | type MeshVendorModel struct { 196 | VendorID uint16 197 | ModelID uint16 198 | SourceAddr uint16 199 | Opcode uint8 200 | Payload []byte 201 | } 202 | 203 | func DecodeMeshVendorModel(b []byte) *MeshVendorModel { 204 | return &MeshVendorModel{ 205 | VendorID: binary.LittleEndian.Uint16(b[6:]), 206 | ModelID: binary.LittleEndian.Uint16(b[8:]), 207 | SourceAddr: binary.LittleEndian.Uint16(b[10:]), 208 | Opcode: b[18], 209 | Payload: b[21:], 210 | } 211 | } 212 | 213 | // MeshStatus evt_mesh_generic_client_server_status 214 | type MeshStatus struct { 215 | ModelID uint16 `json:"mid"` 216 | ElemIndex uint16 `json:"eid"` 217 | ClientAddr uint16 `json:"caddr"` 218 | ServerAddr uint16 `json:"saddr"` 219 | Remain uint32 `json:"remain"` 220 | Flags uint16 `json:"flags"` 221 | Type uint8 `json:"type"` 222 | Params []byte `json:"params"` 223 | } 224 | 225 | func ParseMeshStatus(b []byte) *MeshStatus { 226 | return &MeshStatus{ 227 | ModelID: binary.LittleEndian.Uint16(b[4:]), 228 | ElemIndex: binary.LittleEndian.Uint16(b[6:]), 229 | ClientAddr: binary.LittleEndian.Uint16(b[8:]), 230 | ServerAddr: binary.LittleEndian.Uint16(b[10:]), 231 | Remain: binary.LittleEndian.Uint32(b[12:]), 232 | Flags: binary.LittleEndian.Uint16(b[16:]), 233 | Type: b[18], 234 | Params: b[20:], 235 | } 236 | } 237 | 238 | func ParseIVRecovery(b []byte) (nodeIndex uint32, networkIndex uint32) { 239 | return binary.LittleEndian.Uint32(b[4:]), binary.LittleEndian.Uint32(b[8:]) 240 | } 241 | 242 | func ParseIVUpdateState(b []byte) (result uint16, ivi uint32, state uint8) { 243 | return binary.LittleEndian.Uint16(b[4:]), binary.LittleEndian.Uint32(b[6:]), b[10] 244 | } 245 | 246 | func EncodeGenericClientSet(modelID uint16, serverAddr uint16, type_ uint8, payload interface{}, transition uint32) []byte { 247 | b := []byte{ 248 | 0x20, 0, 0x1E, 1, // cmd_mesh_generic_client_set 249 | 0, 0, // model_id 250 | 0, 0, // elem_index 251 | 0, 0, // server_address 252 | 0, 0, // appkey_index 253 | 0, // tid 254 | 0, 0, 0, 0, // transition 255 | 0, 0, // delay 256 | 1, 0, // flags (response required) 257 | type_, // type 258 | } 259 | binary.LittleEndian.PutUint16(b[4:], modelID) 260 | binary.LittleEndian.PutUint16(b[8:], serverAddr) 261 | binary.LittleEndian.PutUint32(b[13:], transition) 262 | switch payload.(type) { 263 | case uint8: 264 | b = append(b, 1, payload.(uint8)) 265 | case uint16: 266 | b = append(b, 2, 0, 0) 267 | binary.LittleEndian.PutUint16(b[23:], payload.(uint16)) 268 | case uint32: 269 | b = append(b, 4, 0, 0, 0, 0) 270 | binary.LittleEndian.PutUint32(b[23:], payload.(uint32)) 271 | case []byte: 272 | l := len(payload.([]byte)) 273 | b = append(b, uint8(l)) 274 | b = append(b, payload.([]byte)...) 275 | } 276 | b[1] = uint8(len(b) - 4) 277 | return b 278 | } 279 | 280 | func EncodeGenericClientGet(modelID uint16, serverAddr uint16, type_ uint8) []byte { 281 | b := make([]byte, 13) 282 | b[0] = 0x20 // command 283 | b[1] = 9 // main payload len 284 | b[2] = 0x1E // cmd_mesh_generic_client_get 285 | binary.LittleEndian.PutUint16(b[4:], modelID) 286 | binary.LittleEndian.PutUint16(b[8:], serverAddr) 287 | b[12] = type_ 288 | return b 289 | } 290 | 291 | func EncodeMeshVendorModelSend(addr uint16, payload []byte) []byte { 292 | l := uint8(19 - 4 + len(payload)) 293 | b := []byte{ 294 | 0x20, l, 0x19, 0, // cmd_mesh_vendor_model_send 295 | 0, 0, // elem_index 296 | 0x8F, 3, // vendor_id 297 | 1, 0, // model_id 298 | 0, 0, // destination_address 299 | 0, // va_index 300 | 0, 0, // appkey_index 301 | 0, // nonrelayed 302 | 3, // opcode 303 | 1, // final 304 | uint8(len(payload)), 305 | } 306 | binary.LittleEndian.PutUint16(b[10:], addr) 307 | b = append(b, payload...) 308 | return b 309 | } 310 | 311 | func EncodeMeshConfigClientListSubs(addr uint16) []byte { 312 | b := []byte{ 313 | 0x20, 0x09, 0x27, 0x15, // cmd_mesh_config_client_list_subs 314 | 0, 0, // enc_netkey_index 315 | 0, 0, // server_address 316 | 0, // element_index 317 | 0xFF, 0xFF, // vendor_id (use 0xFFFF for Bluetooth SIG models) 318 | 0, 0x10, // model_id 319 | } 320 | binary.LittleEndian.PutUint16(b[6:], addr) 321 | return b 322 | } 323 | 324 | var ParseError = errors.New("parse error") 325 | 326 | // cmd_system_get_bt_address 327 | func DecodeSystemGetBTAddress(b []byte) ([]byte, error) { 328 | if len(b) != 10 || b[1] != 0x06 { 329 | return nil, ParseError 330 | } 331 | return b[4:], nil 332 | } 333 | 334 | // cmd_le_gap_set_discovery_extended_scan_response 335 | func DecodeLeGapSetDiscoveryExtendedScanResponse(b []byte) error { 336 | if len(b) != 6 || b[1] != 0x02 { 337 | return ParseError 338 | } 339 | return nil 340 | } 341 | 342 | // cmd_mesh_node_get_ivupdate_state 343 | func DecodeMeshNodeGetIVUpdateState(b []byte) (result uint16, ivi uint32, state uint8, err error) { 344 | if len(b) != 11 || b[1] != 0x07 { 345 | return 0, 0, 0, ParseError 346 | } 347 | return binary.LittleEndian.Uint16(b[4:]), binary.LittleEndian.Uint32(b[6:]), b[10], nil 348 | } 349 | 350 | // cmd_mesh_config_client_list_subs 351 | func DecodeMeshConfigClientListSubs(b []byte) (uint32, error) { 352 | if len(b) != 10 || b[4] != 0 || b[5] != 0 { 353 | return 0, ParseError 354 | } 355 | return binary.LittleEndian.Uint32(b[6:]), nil 356 | } 357 | 358 | // evt_mesh_config_client_subs_list 359 | func DecodeMeshConfigClientSubsList(b []byte) (uint32, []uint16, error) { 360 | n := uint8(len(b)) 361 | if n < 9 || b[1] < 5 || b[8] != n-9 { 362 | return 0, nil, ParseError 363 | } 364 | n = b[8] / 2 365 | groups := make([]uint16, n) 366 | for i := uint8(0); i < n; i++ { 367 | groups[i] = binary.LittleEndian.Uint16(b[9+i*2:]) 368 | } 369 | handle := binary.LittleEndian.Uint32(b[4:]) 370 | return handle, groups, nil 371 | } 372 | -------------------------------------------------------------------------------- /serial/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /gap/mibeacon.go: -------------------------------------------------------------------------------- 1 | package gap 2 | 3 | import ( 4 | "crypto/aes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "github.com/AlexxIT/gw3/crypt" 9 | ) 10 | 11 | type MiBeacon struct { 12 | Mac string `json:"mac"` 13 | Pdid uint16 `json:"pdid"` 14 | Eid uint16 `json:"eid,omitempty"` 15 | Edata hexbytes `json:"edata,omitempty"` 16 | Seq byte `json:"seq"` 17 | Comment string `json:"comment,omitempty"` 18 | } 19 | 20 | func (b *MiBeacon) Decode() Map { 21 | switch b.Eid { 22 | case 0x1001: // 4097 23 | if len(b.Edata) != 3 { 24 | return nil 25 | } 26 | var action string 27 | switch b.Pdid { 28 | case 950: // Yeelight Dimmer 29 | switch b.Edata[2] { 30 | case 3: 31 | switch b.Edata[0] { 32 | case 0: 33 | switch b.Edata[1] { 34 | case 1: 35 | action = "single" 36 | case 2: 37 | action = "double" 38 | case 3: 39 | action = "triple" 40 | case 4: 41 | action = "quadruple" 42 | case 5: 43 | action = "quintuple" 44 | } 45 | case 1: 46 | return Map{"action": "hold", "duration": b.Edata[1]} 47 | } 48 | case 4: 49 | if b.Edata[0] == 0 { 50 | // rotate with sign (right or left) 51 | angle := int8(b.Edata[1]) 52 | return Map{"action": "rotate", "angle": angle} 53 | } else if b.Edata[1] == 0 { 54 | // hold and rotate with sign (right or left) 55 | angle := int8(b.Edata[0]) 56 | return Map{"action": "rotate_hold", "angle": angle} 57 | } 58 | } 59 | case 1249: // Xiaomi Magic Cube 60 | switch b.Edata[0] { 61 | case 0: 62 | action = "right" 63 | case 1: 64 | action = "left" 65 | } 66 | case 1983: // Yeelight Button S1 67 | switch b.Edata[2] { 68 | case 0: 69 | action = "single" 70 | case 1: 71 | action = "double" 72 | case 2: 73 | action = "hold" 74 | } 75 | default: 76 | if b.Edata[0] == 0 { 77 | // Xiaomi Motion Sensor 2, Xiaomi Water Leak Sensor 78 | action = "single" 79 | } else { 80 | // all unknown devices 81 | action = fmt.Sprintf("%x", b.Edata) 82 | } 83 | } 84 | if action != "" { 85 | return Map{"action": action} 86 | } 87 | case 0x1002: // 4098 88 | // No sleep (0x00), falling asleep (0x01) 89 | return Map{"sleep": b.Edata[0]} 90 | case 0x1003: // 4099 91 | return Map{"rssi": int8(b.Edata[0])} 92 | case 0x1004: // 4100 93 | if len(b.Edata) == 2 { 94 | value := float32(int16(binary.LittleEndian.Uint16(b.Edata))) / 10 95 | return Map{"temperature": value} 96 | } 97 | case 0x1005: // 4101 98 | if len(b.Edata) == 2 { 99 | // Kettle, thanks https://github.com/custom-components/ble_monitor/ 100 | return Map{"power": b.Edata[0], "temperature": float32(b.Edata[1])} 101 | } 102 | case 0x1006: // 4102 103 | if len(b.Edata) == 2 { 104 | value := float32(binary.LittleEndian.Uint16(b.Edata)) / 10 105 | if b.Pdid == 903 || b.Pdid == 1371 { 106 | // two models has bug, they increase humidity on each data by 0.1 107 | value = float32(int32(value)) 108 | } 109 | return Map{"humidity": value} 110 | } 111 | case 0x1007: // 4103 112 | if len(b.Edata) == 3 { 113 | value := uint32(b.Edata[0]) | uint32(b.Edata[1])<<8 | uint32(b.Edata[2])<<16 114 | if b.Pdid == 2038 { 115 | // Night Light 2: 1 - no light, 100 - light 116 | if value >= 100 { 117 | return Map{"light": 1} 118 | } else { 119 | return Map{"light": 0} 120 | } 121 | } 122 | // Range: 0-120000, lux 123 | return Map{"illuminance": value} 124 | } 125 | case 0x1008: // 4104 126 | // Humidity percentage, range: 0-100 127 | return Map{"moisture": b.Edata[0]} 128 | case 0x1009: // 4105 129 | if len(b.Edata) == 2 { 130 | // Soil EC value, Unit us/cm, range: 0-5000 131 | value := binary.LittleEndian.Uint16(b.Edata) 132 | return Map{"conductivity": value} 133 | } 134 | case 0x100A: // 4106 135 | return Map{"battery": b.Edata[0]} 136 | case 0x100D: // 4109 137 | if len(b.Edata) == 4 { 138 | value1 := float32(int16(binary.LittleEndian.Uint16(b.Edata))) / 10 139 | value2 := float32(binary.LittleEndian.Uint16(b.Edata[2:])) / 10 140 | return Map{"temperature": value1, "humidity": value2} 141 | } 142 | case 0x100E: // 4110 143 | // 1 => true => on => unlocked 144 | if b.Edata[0] == 0 { 145 | return Map{"lock": 1} 146 | } else { 147 | return Map{"lock": 0} 148 | } 149 | case 0x100F: // 4111 150 | // 1 => true => on => dooor opening 151 | if b.Edata[0] == 0 { 152 | return Map{"opening": 1} 153 | } else { 154 | return Map{"opening": 0} 155 | } 156 | case 0x1010: // 4112 157 | if len(b.Edata) == 2 { 158 | value := float32(int16(binary.LittleEndian.Uint16(b.Edata))) / 100 159 | return Map{"formaldehyde": value} 160 | } 161 | case 0x1012: // 4114 162 | // 1 => true => open 163 | return Map{"opening": b.Edata[0]} 164 | case 0x1013: // 4115 165 | // Remaining percentage, range 0~100 166 | return Map{"supply": b.Edata[0]} 167 | case 0x1014: // 4116 168 | // 1 => on => wet 169 | return Map{"water_leak": b.Edata[0]} 170 | case 0x1015: // 4117 171 | // 1 => on => alarm 172 | return Map{"smoke": b.Edata[0]} 173 | case 0x1016: // 4118 174 | // 1 => on => alarm 175 | return Map{"gas": b.Edata[0]} 176 | case 0x1017: // 4119 177 | if len(b.Edata) == 4 { 178 | // The duration of the unmanned state, in seconds 179 | value := binary.LittleEndian.Uint32(b.Edata) 180 | return Map{"idle_time": value} 181 | } 182 | case 0x1018: // 4120 183 | // Door Sensor 2: 0 - dark, 1 - light 184 | return Map{"light": b.Edata[0]} 185 | case 0x1019: // 4121 186 | // 0x00: open the door, 0x01: close the door, 187 | // 0x02: not closed after timeout, 0x03: device reset 188 | // 1 => true => open 189 | switch b.Edata[0] { 190 | case 0: 191 | return Map{"contact": 1} 192 | case 1: 193 | return Map{"contact": 0} 194 | } 195 | case 0x06: 196 | if len(b.Edata) == 5 { 197 | actionID := b.Edata[4] 198 | keyID := binary.LittleEndian.Uint32(b.Edata) 199 | return Map{ 200 | "action": "fingerprint", 201 | "action_id": actionID, 202 | "key_id": fmt.Sprintf("%04x", keyID), 203 | "message": mibeaconFingerprintAction(actionID), 204 | } 205 | } 206 | case 0x07: 207 | actionID := b.Edata[0] 208 | return Map{ 209 | "action": "door", 210 | "action_id": actionID, 211 | "message": mibeaconDoorAction(actionID), 212 | } 213 | case 0x0008: 214 | if b.Edata[0] > 0 { 215 | return Map{"action": "armed", "state": true} 216 | } else { 217 | return Map{"action": "armed", "state": false} 218 | } 219 | case 0x0B: // 11 220 | var keyID string 221 | actionID := b.Edata[0] & 0xF 222 | methodID := b.Edata[0] >> 4 223 | key := binary.LittleEndian.Uint32(b.Edata[1:]) 224 | err := mibeaconLockError(key) 225 | if err == "" && methodID > 0 { 226 | keyID = fmt.Sprintf("%d", key&0xFFFF) 227 | } else { 228 | keyID = fmt.Sprintf("%04x", key) 229 | } 230 | timestamp := binary.LittleEndian.Uint32(b.Edata[5:]) 231 | return Map{ 232 | "action": "lock", 233 | "action_id": actionID, 234 | "method_id": methodID, 235 | "message": mibeaconLockAction(actionID), 236 | "method": mibeaconLockMethod(methodID), 237 | "key_id": keyID, 238 | "error": err, 239 | "timestamp": timestamp, 240 | } 241 | case 0x0F: // 15 242 | if len(b.Edata) == 3 { 243 | // Night Light 2: 1 - moving no light, 100 - moving with light 244 | // Motion Sensor 2: 0 - moving no light, 256 - moving with light 245 | // Qingping Motion Sensor - moving with illuminance data 246 | value := uint32(b.Edata[0]) | uint32(b.Edata[1])<<8 | uint32(b.Edata[2])<<16 247 | if b.Pdid == 2691 { 248 | return Map{"action": "motion", "motion": 1, "illuminance": value} 249 | } else if value >= 100 { 250 | return Map{"action": "motion", "motion": 1, "light": 1} 251 | } else { 252 | return Map{"action": "motion", "motion": 1, "light": 0} 253 | } 254 | } 255 | case 0x10: 256 | if len(b.Edata) == 2 { 257 | // Toothbrush Т500 258 | if b.Edata[0] == 0 { 259 | return Map{"action": "start", "counter": b.Edata[1]} 260 | } else { 261 | return Map{"action": "finish", "score": b.Edata[1]} 262 | } 263 | } 264 | } 265 | return nil 266 | } 267 | 268 | func ParseMiBeacon(data []byte, getBindkey func(string) string) (mibeacon *MiBeacon, useful byte) { 269 | // https://iot.mi.com/new/doc/embedded-development/ble/ble-mibeacon 270 | mibeacon = &MiBeacon{ 271 | Pdid: binary.LittleEndian.Uint16(data[2:]), 272 | Seq: data[4], 273 | } 274 | 275 | frame := binary.LittleEndian.Uint16(data) 276 | // check mac 277 | if frame&0x10 == 0 { 278 | mibeacon.Comment = "no mac" 279 | return mibeacon, 0 280 | } 281 | 282 | mibeacon.Mac = SprintMAC(data[5:]) 283 | 284 | // check payload 285 | if frame&0x40 == 0 { 286 | mibeacon.Comment = "no payload" 287 | return mibeacon, 0 288 | } 289 | 290 | version := (frame >> 12) & 0b1111 291 | 292 | i := 5 + 6 293 | 294 | // check capability 295 | if frame&0x20 > 0 { 296 | capab := data[i] 297 | i++ 298 | if (capab >> 3) == 0b11 { 299 | i += 2 300 | } 301 | if version == 5 && capab&0x20 > 0 { 302 | i += 2 303 | } 304 | } 305 | 306 | var payload []byte 307 | 308 | // check encryption 309 | if frame&0x08 > 0 { 310 | // keys can be nil, no problem 311 | key := getBindkey(mibeacon.Mac) 312 | if key != "" { 313 | switch version { 314 | case 2, 3: 315 | payload = mibeaconDecode1(data, i, key) 316 | case 4, 5: 317 | payload = mibeaconDecode4(data, i, key) 318 | } 319 | if payload == nil { 320 | mibeacon.Comment = "wrong enc key" 321 | return mibeacon, 1 322 | } 323 | } else { 324 | mibeacon.Comment = "encrypted" 325 | return mibeacon, 1 326 | } 327 | } else if version == 5 && frame&0x80 > 0 { 328 | payload = data[i : len(data)-2] 329 | } else { 330 | payload = data[i:] 331 | } 332 | 333 | if len(payload) < 4 { 334 | mibeacon.Edata = payload 335 | mibeacon.Comment = "small payload" 336 | return mibeacon, 0 337 | } 338 | 339 | // skip payload len check because ATC_MiThermometer has wrong payload 340 | //if payload[2] != byte(len(payload))-3 { 341 | // mibeacon.Edata = hex.EncodeToString(payload]) 342 | // mibeacon.Comment = "wrong len payload" 343 | // return mibeacon, 1 344 | //} 345 | 346 | mibeacon.Eid = binary.LittleEndian.Uint16(payload) 347 | mibeacon.Edata = payload[3:] 348 | 349 | return mibeacon, 2 350 | } 351 | 352 | func mibeaconDecode1(mibeacon []byte, pos int, key string) []byte { 353 | key2, _ := hex.DecodeString(key) 354 | key3 := make([]byte, 0, 16) 355 | key3 = append(key3, key2[:6]...) 356 | key3 = append(key3, 0x8d, 0x3d, 0x3c, 0x97) 357 | key3 = append(key3, key2[6:12]...) 358 | c, err := aes.NewCipher(key3) 359 | if err != nil { 360 | return nil 361 | } 362 | 363 | nonce := make([]byte, 0, 13) 364 | // frame + pdid + cnt 365 | nonce = append(nonce, mibeacon[0:5]...) 366 | // counter 367 | nonce = append(nonce, mibeacon[len(mibeacon)-4:len(mibeacon)-1]...) 368 | // mac5 369 | nonce = append(nonce, mibeacon[5:10]...) 370 | 371 | // witout tag validating, because tag only 1 byte len 372 | ccm, err := crypt.NewCCMWithNonceAndTagSizes(c, len(nonce), 0) 373 | if err != nil { 374 | return nil 375 | } 376 | 377 | ciphertext := mibeacon[pos : len(mibeacon)-4] 378 | 379 | plain, err := ccm.Open(nil, nonce, ciphertext, []byte{0x11}) 380 | if err != nil { 381 | return nil 382 | } 383 | 384 | return plain 385 | } 386 | 387 | func mibeaconDecode4(mibeacon []byte, pos int, key string) []byte { 388 | key2, _ := hex.DecodeString(key) 389 | c, err := aes.NewCipher(key2) 390 | if err != nil { 391 | return nil 392 | } 393 | 394 | nonce := make([]byte, 0, 12) 395 | // mac 396 | nonce = append(nonce, mibeacon[5:11]...) 397 | // pdid + seq 398 | nonce = append(nonce, mibeacon[2:5]...) 399 | // counter 400 | nonce = append(nonce, mibeacon[len(mibeacon)-7:len(mibeacon)-4]...) 401 | 402 | ccm, err := crypt.NewCCMWithNonceAndTagSizes(c, len(nonce), 4) 403 | if err != nil { 404 | return nil 405 | } 406 | 407 | ciphertext := mibeacon[pos : len(mibeacon)-7] 408 | // cipertext should contain token/tag at the end (4 bytes) 409 | ciphertext = append(ciphertext, mibeacon[len(mibeacon)-4:]...) 410 | 411 | plain, err := ccm.Open(nil, nonce, ciphertext, []byte{0x11}) 412 | if err != nil { 413 | return nil 414 | } 415 | 416 | return plain 417 | } 418 | 419 | func mibeaconFingerprintAction(actionID uint8) string { 420 | switch actionID { 421 | case 0: 422 | return "Match successful" 423 | case 1: 424 | return "Match failed" 425 | case 2: 426 | return "Timeout" 427 | case 3: 428 | return "Low quality" 429 | case 4: 430 | return "Insufficient area" 431 | case 5: 432 | return "Skin is too dry" 433 | case 6: 434 | return "Skin is too wet" 435 | } 436 | return "" 437 | } 438 | 439 | func mibeaconDoorAction(actionID uint8) string { 440 | switch actionID { 441 | case 0: 442 | return "Door is open" 443 | case 1: 444 | return "Door is closed" 445 | case 2: 446 | return "Timeout is not closed" 447 | case 3: 448 | return "Knock on the door" 449 | case 4: 450 | return "Breaking the door" 451 | case 5: 452 | return "Door is stuck" 453 | } 454 | return "" 455 | } 456 | 457 | func mibeaconLockAction(actionID uint8) string { 458 | switch actionID { 459 | case 0b0000: 460 | return "Unlock outside the door" 461 | case 0b0001: 462 | return "Lock" 463 | case 0b0010: 464 | return "Turn on anti-lock" 465 | case 0b0011: 466 | return "Turn off anti-lock" 467 | case 0b0100: 468 | return "Unlock inside the door" 469 | case 0b0101: 470 | return "Lock inside the door" 471 | case 0b0110: 472 | return "Turn on child lock" 473 | case 0b0111: 474 | return "Turn off child lock" 475 | case 0b1111: 476 | return "-" 477 | } 478 | return "" 479 | } 480 | 481 | func mibeaconLockMethod(methodID uint8) string { 482 | switch methodID { 483 | case 0b0000: 484 | return "bluetooth" 485 | case 0b0001: 486 | return "password" 487 | case 0b0010: 488 | return "biological" 489 | case 0b0011: 490 | return "key" 491 | case 0b0100: 492 | return "turntable" 493 | case 0b0101: 494 | return "nfc" 495 | case 0b0110: 496 | return "one-time password" 497 | case 0b0111: 498 | return "two-step verification" 499 | case 0b1000: 500 | return "coercion" 501 | case 0b1010: 502 | return "manual" 503 | case 0b1011: 504 | return "automatic" 505 | case 0b1111: 506 | return "-" 507 | } 508 | return "" 509 | } 510 | 511 | func mibeaconLockError(keyID uint32) string { 512 | switch keyID { 513 | case 0xC0DE0000: 514 | return "Frequent unlocking with incorrect password" 515 | case 0xC0DE0001: 516 | return "Frequent unlocking with wrong fingerprints" 517 | case 0xC0DE0002: 518 | return "Operation timeout (password input timeout)" 519 | case 0xC0DE0003: 520 | return "Lock picking" 521 | case 0xC0DE0004: 522 | return "Reset button is pressed" 523 | case 0xC0DE0005: 524 | return "The wrong key is frequently unlocked" 525 | case 0xC0DE0006: 526 | return "Foreign body in the keyhole" 527 | case 0xC0DE0007: 528 | return "The key has not been taken out" 529 | case 0xC0DE0008: 530 | return "Error NFC frequently unlocks" 531 | case 0xC0DE0009: 532 | return "Timeout is not locked as required" 533 | case 0xC0DE000A: 534 | return "Failure to unlock frequently in multiple ways" 535 | case 0xC0DE000B: 536 | return "Unlocking the face frequently fails" 537 | case 0xC0DE000C: 538 | return "Failure to unlock the vein frequently" 539 | case 0xC0DE000D: 540 | return "Hijacking alarm" 541 | case 0xC0DE000E: 542 | return "Unlock inside the door after arming" 543 | case 0xC0DE000F: 544 | return "Palmprints frequently fail to unlock" 545 | case 0xC0DE0010: 546 | return "The safe was moved" 547 | case 0xC0DE1000: 548 | return "The battery level is less than 10%" 549 | case 0xC0DE1001: 550 | return "The battery is less than 5%" 551 | case 0xC0DE1002: 552 | return "The fingerprint sensor is abnormal" 553 | case 0xC0DE1003: 554 | return "The accessory battery is low" 555 | case 0xC0DE1004: 556 | return "Mechanical failure" 557 | } 558 | return "" 559 | } 560 | -------------------------------------------------------------------------------- /mqtt/mqtt.go: -------------------------------------------------------------------------------- 1 | // Package mqtt implements MQTT clients and servers. 2 | package mqtt 3 | 4 | import ( 5 | crand "crypto/rand" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | "math/rand" 11 | "net" 12 | "runtime" 13 | "strings" 14 | "sync" 15 | "sync/atomic" 16 | "time" 17 | 18 | proto "github.com/huin/mqtt" 19 | ) 20 | 21 | // A random number generator ready to make client-id's, if 22 | // they do not provide them to us. 23 | var cliRand *rand.Rand 24 | 25 | func init() { 26 | var seed int64 27 | var sb [4]byte 28 | crand.Read(sb[:]) 29 | seed = int64(time.Now().Nanosecond())<<32 | 30 | int64(sb[0])<<24 | int64(sb[1])<<16 | 31 | int64(sb[2])<<8 | int64(sb[3]) 32 | cliRand = rand.New(rand.NewSource(seed)) 33 | } 34 | 35 | type stats struct { 36 | recv int64 37 | sent int64 38 | clients int64 39 | clientsMax int64 40 | lastmsgs int64 41 | } 42 | 43 | func (s *stats) messageRecv() { atomic.AddInt64(&s.recv, 1) } 44 | func (s *stats) messageSend() { atomic.AddInt64(&s.sent, 1) } 45 | func (s *stats) clientConnect() { atomic.AddInt64(&s.clients, 1) } 46 | func (s *stats) clientDisconnect() { atomic.AddInt64(&s.clients, -1) } 47 | 48 | func statsMessage(topic string, stat int64) *proto.Publish { 49 | return &proto.Publish{ 50 | Header: header(dupFalse, proto.QosAtMostOnce, retainTrue), 51 | TopicName: topic, 52 | Payload: newIntPayload(stat), 53 | } 54 | } 55 | 56 | func (s *stats) publish(sub *subscriptions, interval time.Duration) { 57 | clients := atomic.LoadInt64(&s.clients) 58 | clientsMax := atomic.LoadInt64(&s.clientsMax) 59 | if clients > clientsMax { 60 | clientsMax = clients 61 | atomic.StoreInt64(&s.clientsMax, clientsMax) 62 | } 63 | sub.submit(nil, statsMessage("$SYS/broker/clients/active", clients)) 64 | sub.submit(nil, statsMessage("$SYS/broker/clients/maximum", clientsMax)) 65 | sub.submit(nil, statsMessage("$SYS/broker/messages/received", 66 | atomic.LoadInt64(&s.recv))) 67 | sub.submit(nil, statsMessage("$SYS/broker/messages/sent", 68 | atomic.LoadInt64(&s.sent))) 69 | 70 | msgs := atomic.LoadInt64(&s.recv) + atomic.LoadInt64(&s.sent) 71 | msgpersec := (msgs - s.lastmsgs) / int64(interval/time.Second) 72 | // no need for atomic because we are the only reader/writer of it 73 | s.lastmsgs = msgs 74 | 75 | sub.submit(nil, statsMessage("$SYS/broker/messages/per-sec", msgpersec)) 76 | } 77 | 78 | // An intPayload implements proto.Payload, and is an int64 that 79 | // formats itself and then prints itself into the payload. 80 | type intPayload string 81 | 82 | func newIntPayload(i int64) intPayload { 83 | return intPayload(fmt.Sprint(i)) 84 | } 85 | func (ip intPayload) ReadPayload(r io.Reader) error { 86 | // not implemented 87 | return nil 88 | } 89 | func (ip intPayload) WritePayload(w io.Writer) error { 90 | _, err := w.Write([]byte(string(ip))) 91 | return err 92 | } 93 | func (ip intPayload) Size() int { 94 | return len(ip) 95 | } 96 | 97 | // A retain holds information necessary to correctly manage retained 98 | // messages. 99 | // 100 | // This needs to hold copies of the proto.Publish, not pointers to 101 | // it, or else we can send out one with the wrong retain flag. 102 | type retain struct { 103 | m proto.Publish 104 | wild wild 105 | } 106 | 107 | type subscriptions struct { 108 | workers int 109 | posts chan post 110 | 111 | mu sync.Mutex // guards access to fields below 112 | subs map[string][]*incomingConn 113 | wildcards []wild 114 | retain map[string]retain 115 | stats *stats 116 | } 117 | 118 | // The length of the queue that subscription processing 119 | // workers are taking from. 120 | const postQueue = 100 121 | 122 | func newSubscriptions(workers int) *subscriptions { 123 | s := &subscriptions{ 124 | subs: make(map[string][]*incomingConn), 125 | retain: make(map[string]retain), 126 | posts: make(chan post, postQueue), 127 | workers: workers, 128 | } 129 | for i := 0; i < s.workers; i++ { 130 | go s.run(i) 131 | } 132 | return s 133 | } 134 | 135 | func (s *subscriptions) sendRetain(topic string, c *incomingConn) { 136 | s.mu.Lock() 137 | var tlist []string 138 | if isWildcard(topic) { 139 | 140 | // TODO: select matching topics from the retain map 141 | } else { 142 | tlist = []string{topic} 143 | } 144 | for _, t := range tlist { 145 | if r, ok := s.retain[t]; ok { 146 | c.submit(&r.m) 147 | } 148 | } 149 | s.mu.Unlock() 150 | } 151 | 152 | func (s *subscriptions) add(topic string, c *incomingConn) { 153 | s.mu.Lock() 154 | defer s.mu.Unlock() 155 | if isWildcard(topic) { 156 | w := newWild(topic, c) 157 | if w.valid() { 158 | s.wildcards = append(s.wildcards, w) 159 | } 160 | } else { 161 | s.subs[topic] = append(s.subs[topic], c) 162 | } 163 | } 164 | 165 | type wild struct { 166 | wild []string 167 | c *incomingConn 168 | } 169 | 170 | func newWild(topic string, c *incomingConn) wild { 171 | return wild{wild: strings.Split(topic, "/"), c: c} 172 | } 173 | 174 | func (w wild) matches(parts []string) bool { 175 | i := 0 176 | for i < len(parts) { 177 | // topic is longer, no match 178 | if i >= len(w.wild) { 179 | return false 180 | } 181 | // matched up to here, and now the wildcard says "all others will match" 182 | if w.wild[i] == "#" { 183 | return true 184 | } 185 | // text does not match, and there wasn't a + to excuse it 186 | if parts[i] != w.wild[i] && w.wild[i] != "+" { 187 | return false 188 | } 189 | i++ 190 | } 191 | 192 | // make finance/stock/ibm/# match finance/stock/ibm 193 | if i == len(w.wild)-1 && w.wild[len(w.wild)-1] == "#" { 194 | return true 195 | } 196 | 197 | if i == len(w.wild) { 198 | return true 199 | } 200 | return false 201 | } 202 | 203 | // Find all connections that are subscribed to this topic. 204 | func (s *subscriptions) subscribers(topic string) []*incomingConn { 205 | s.mu.Lock() 206 | defer s.mu.Unlock() 207 | 208 | // non-wildcard subscribers 209 | res := s.subs[topic] 210 | 211 | // process wildcards 212 | parts := strings.Split(topic, "/") 213 | for _, w := range s.wildcards { 214 | if w.matches(parts) { 215 | res = append(res, w.c) 216 | } 217 | } 218 | 219 | return res 220 | } 221 | 222 | // Remove all subscriptions that refer to a connection. 223 | func (s *subscriptions) unsubAll(c *incomingConn) { 224 | s.mu.Lock() 225 | for _, v := range s.subs { 226 | for i := range v { 227 | if v[i] == c { 228 | v[i] = nil 229 | } 230 | } 231 | } 232 | 233 | // remove any associated entries in the wildcard list 234 | var wildNew []wild 235 | for i := 0; i < len(s.wildcards); i++ { 236 | if s.wildcards[i].c != c { 237 | wildNew = append(wildNew, s.wildcards[i]) 238 | } 239 | } 240 | s.wildcards = wildNew 241 | 242 | s.mu.Unlock() 243 | } 244 | 245 | // Remove the subscription to topic for a given connection. 246 | func (s *subscriptions) unsub(topic string, c *incomingConn) { 247 | s.mu.Lock() 248 | if subs, ok := s.subs[topic]; ok { 249 | nils := 0 250 | 251 | // Search the list, removing references to our connection. 252 | // At the same time, count the nils to see if this list is now empty. 253 | for i := 0; i < len(subs); i++ { 254 | if subs[i] == c { 255 | subs[i] = nil 256 | } 257 | if subs[i] == nil { 258 | nils++ 259 | } 260 | } 261 | 262 | if nils == len(subs) { 263 | delete(s.subs, topic) 264 | } 265 | } 266 | s.mu.Unlock() 267 | } 268 | 269 | // The subscription processing worker. 270 | func (s *subscriptions) run(id int) { 271 | tag := fmt.Sprintf("worker %d ", id) 272 | log.Print(tag, "started") 273 | for post := range s.posts { 274 | // Remember the original retain setting, but send out immediate 275 | // copies without retain: "When a server sends a PUBLISH to a client 276 | // as a result of a subscription that already existed when the 277 | // original PUBLISH arrived, the Retain flag should not be set, 278 | // regardless of the Retain flag of the original PUBLISH. 279 | isRetain := post.m.Header.Retain 280 | post.m.Header.Retain = false 281 | 282 | // Handle "retain with payload size zero = delete retain". 283 | // Once the delete is done, return instead of continuing. 284 | if isRetain && post.m.Payload.Size() == 0 { 285 | s.mu.Lock() 286 | delete(s.retain, post.m.TopicName) 287 | s.mu.Unlock() 288 | return 289 | } 290 | 291 | // Find all the connections that should be notified of this message. 292 | conns := s.subscribers(post.m.TopicName) 293 | 294 | // Queue the outgoing messages 295 | for _, c := range conns { 296 | // Do not echo messages back to where they came from. 297 | if c == post.c { 298 | continue 299 | } 300 | 301 | if c != nil { 302 | c.submit(post.m) 303 | } 304 | } 305 | 306 | if isRetain { 307 | s.mu.Lock() 308 | // Save a copy of it, and set that copy's Retain to true, so that 309 | // when we send it out later we notify new subscribers that this 310 | // is an old message. 311 | msg := *post.m 312 | msg.Header.Retain = true 313 | s.retain[post.m.TopicName] = retain{m: msg} 314 | s.mu.Unlock() 315 | } 316 | } 317 | } 318 | 319 | func (s *subscriptions) submit(c *incomingConn, m *proto.Publish) { 320 | s.posts <- post{c: c, m: m} 321 | } 322 | 323 | // A post is a unit of work for the subscription processing workers. 324 | type post struct { 325 | c *incomingConn 326 | m *proto.Publish 327 | } 328 | 329 | // A Server holds all the state associated with an MQTT server. 330 | type Server struct { 331 | l net.Listener 332 | subs *subscriptions 333 | stats *stats 334 | Done chan struct{} 335 | StatsInterval time.Duration // Defaults to 10 seconds. Must be set using sync/atomic.StoreInt64(). 336 | Dump bool // When true, dump the messages in and out. 337 | rand *rand.Rand 338 | } 339 | 340 | // NewServer creates a new MQTT server, which accepts connections from 341 | // the given listener. When the server is stopped (for instance by 342 | // another goroutine closing the net.Listener), channel Done will become 343 | // readable. 344 | func NewServer(l net.Listener) *Server { 345 | svr := &Server{ 346 | l: l, 347 | stats: &stats{}, 348 | Done: make(chan struct{}), 349 | StatsInterval: time.Second * 10, 350 | subs: newSubscriptions(runtime.GOMAXPROCS(0)), 351 | } 352 | 353 | // start the stats reporting goroutine 354 | go func() { 355 | for { 356 | svr.stats.publish(svr.subs, svr.StatsInterval) 357 | select { 358 | case <-svr.Done: 359 | return 360 | default: 361 | // keep going 362 | } 363 | time.Sleep(svr.StatsInterval) 364 | } 365 | }() 366 | 367 | return svr 368 | } 369 | 370 | // Start makes the Server start accepting and handling connections. 371 | func (s *Server) Start() { 372 | go func() { 373 | for { 374 | conn, err := s.l.Accept() 375 | if err != nil { 376 | log.Print("Accept: ", err) 377 | break 378 | } 379 | 380 | cli := s.newIncomingConn(conn) 381 | s.stats.clientConnect() 382 | cli.start() 383 | } 384 | close(s.Done) 385 | }() 386 | } 387 | 388 | // An IncomingConn represents a connection into a Server. 389 | type incomingConn struct { 390 | svr *Server 391 | conn net.Conn 392 | jobs chan job 393 | clientid string 394 | Done chan struct{} 395 | } 396 | 397 | var clients = make(map[string]*incomingConn) 398 | var clientsMu sync.Mutex 399 | 400 | const sendingQueueLength = 10000 401 | 402 | // newIncomingConn creates a new incomingConn associated with this 403 | // server. The connection becomes the property of the incomingConn 404 | // and should not be touched again by the caller until the Done 405 | // channel becomes readable. 406 | func (s *Server) newIncomingConn(conn net.Conn) *incomingConn { 407 | return &incomingConn{ 408 | svr: s, 409 | conn: conn, 410 | jobs: make(chan job, sendingQueueLength), 411 | Done: make(chan struct{}), 412 | } 413 | } 414 | 415 | type receipt chan struct{} 416 | 417 | // Wait for the receipt to indicate that the job is done. 418 | func (r receipt) wait() { 419 | // TODO: timeout 420 | <-r 421 | } 422 | 423 | type job struct { 424 | m proto.Message 425 | r receipt 426 | } 427 | 428 | // Start reading and writing on this connection. 429 | func (c *incomingConn) start() { 430 | go c.reader() 431 | go c.writer() 432 | } 433 | 434 | // Add this connection to the map, or find out that an existing connection 435 | // already exists for the same client-id. 436 | func (c *incomingConn) add() *incomingConn { 437 | clientsMu.Lock() 438 | defer clientsMu.Unlock() 439 | 440 | existing, ok := clients[c.clientid] 441 | if ok { 442 | // this client id already exists, return it 443 | return existing 444 | } 445 | 446 | clients[c.clientid] = c 447 | return nil 448 | } 449 | 450 | // Delete a connection; the connection must be closed by the caller first. 451 | func (c *incomingConn) del() { 452 | clientsMu.Lock() 453 | delete(clients, c.clientid) 454 | clientsMu.Unlock() 455 | return 456 | } 457 | 458 | // Queue a message; no notification of sending is done. 459 | func (c *incomingConn) submit(m proto.Message) { 460 | j := job{m: m} 461 | select { 462 | case c.jobs <- j: 463 | default: 464 | log.Print(c, ": failed to submit message") 465 | } 466 | return 467 | } 468 | 469 | func (c *incomingConn) String() string { 470 | return fmt.Sprintf("{IncomingConn: %v}", c.clientid) 471 | } 472 | 473 | // Queue a message, returns a channel that will be readable 474 | // when the message is sent. 475 | func (c *incomingConn) submitSync(m proto.Message) receipt { 476 | j := job{m: m, r: make(receipt)} 477 | c.jobs <- j 478 | return j.r 479 | } 480 | 481 | func (c *incomingConn) reader() { 482 | // On exit, close the connection and arrange for the writer to exit 483 | // by closing the output channel. 484 | defer func() { 485 | c.conn.Close() 486 | c.svr.stats.clientDisconnect() 487 | close(c.jobs) 488 | }() 489 | 490 | for { 491 | // TODO: timeout (first message and/or keepalives) 492 | m, err := proto.DecodeOneMessage(c.conn, nil) 493 | if err != nil { 494 | if err == io.EOF { 495 | return 496 | } 497 | if strings.HasSuffix(err.Error(), "use of closed network connection") { 498 | return 499 | } 500 | log.Print("reader: ", err) 501 | return 502 | } 503 | c.svr.stats.messageRecv() 504 | 505 | if c.svr.Dump { 506 | log.Printf("dump in: %T", m) 507 | } 508 | 509 | switch m := m.(type) { 510 | case *proto.Connect: 511 | rc := proto.RetCodeAccepted 512 | 513 | if m.ProtocolName != "MQIsdp" || 514 | m.ProtocolVersion != 3 { 515 | log.Print("reader: reject connection from ", m.ProtocolName, " version ", m.ProtocolVersion) 516 | rc = proto.RetCodeUnacceptableProtocolVersion 517 | } 518 | 519 | // Check client id. 520 | if len(m.ClientId) < 1 || len(m.ClientId) > 23 { 521 | rc = proto.RetCodeIdentifierRejected 522 | } 523 | c.clientid = m.ClientId 524 | 525 | // Disconnect existing connections. 526 | if existing := c.add(); existing != nil { 527 | disconnect := &proto.Disconnect{} 528 | r := existing.submitSync(disconnect) 529 | r.wait() 530 | c.add() 531 | } 532 | 533 | // TODO: Last will 534 | 535 | connack := &proto.ConnAck{ 536 | ReturnCode: rc, 537 | } 538 | c.submit(connack) 539 | 540 | // close connection if it was a bad connect 541 | if rc != proto.RetCodeAccepted { 542 | log.Printf("Connection refused for %v: %v", c.conn.RemoteAddr(), ConnectionErrors[rc]) 543 | return 544 | } 545 | 546 | // Log in mosquitto format. 547 | clean := 0 548 | if m.CleanSession { 549 | clean = 1 550 | } 551 | log.Printf("New client connected from %v as %v (c%v, k%v).", c.conn.RemoteAddr(), c.clientid, clean, m.KeepAliveTimer) 552 | 553 | case *proto.Publish: 554 | // TODO: Proper QoS support 555 | if m.Header.QosLevel != proto.QosAtMostOnce { 556 | log.Printf("reader: no support for QoS %v yet", m.Header.QosLevel) 557 | return 558 | } 559 | if m.Header.QosLevel != proto.QosAtMostOnce && m.MessageId == 0 { 560 | // Invalid message ID. See MQTT-2.3.1-1. 561 | log.Printf("reader: invalid MessageId in PUBLISH.") 562 | return 563 | } 564 | if isWildcard(m.TopicName) { 565 | log.Print("reader: ignoring PUBLISH with wildcard topic ", m.TopicName) 566 | } else { 567 | c.svr.subs.submit(c, m) 568 | } 569 | c.submit(&proto.PubAck{MessageId: m.MessageId}) 570 | 571 | case *proto.PingReq: 572 | c.submit(&proto.PingResp{}) 573 | 574 | case *proto.Subscribe: 575 | if m.Header.QosLevel != proto.QosAtLeastOnce { 576 | // protocol error, disconnect 577 | return 578 | } 579 | if m.MessageId == 0 { 580 | // Invalid message ID. See MQTT-2.3.1-1. 581 | log.Printf("reader: invalid MessageId in SUBSCRIBE.") 582 | return 583 | } 584 | suback := &proto.SubAck{ 585 | MessageId: m.MessageId, 586 | TopicsQos: make([]proto.QosLevel, len(m.Topics)), 587 | } 588 | for i, tq := range m.Topics { 589 | // TODO: Handle varying QoS correctly 590 | c.svr.subs.add(tq.Topic, c) 591 | suback.TopicsQos[i] = proto.QosAtMostOnce 592 | } 593 | c.submit(suback) 594 | 595 | // Process retained messages. 596 | for _, tq := range m.Topics { 597 | c.svr.subs.sendRetain(tq.Topic, c) 598 | } 599 | 600 | case *proto.Unsubscribe: 601 | if m.Header.QosLevel != proto.QosAtMostOnce && m.MessageId == 0 { 602 | // Invalid message ID. See MQTT-2.3.1-1. 603 | log.Printf("reader: invalid MessageId in UNSUBSCRIBE.") 604 | return 605 | } 606 | for _, t := range m.Topics { 607 | c.svr.subs.unsub(t, c) 608 | } 609 | ack := &proto.UnsubAck{MessageId: m.MessageId} 610 | c.submit(ack) 611 | 612 | case *proto.Disconnect: 613 | return 614 | 615 | default: 616 | log.Printf("reader: unknown msg type %T", m) 617 | return 618 | } 619 | } 620 | } 621 | 622 | func (c *incomingConn) writer() { 623 | 624 | // Close connection on exit in order to cause reader to exit. 625 | defer func() { 626 | c.conn.Close() 627 | c.del() 628 | c.svr.subs.unsubAll(c) 629 | }() 630 | 631 | for job := range c.jobs { 632 | if c.svr.Dump { 633 | log.Printf("dump out: %T", job.m) 634 | } 635 | 636 | // TODO: write timeout 637 | err := job.m.Encode(c.conn) 638 | if job.r != nil { 639 | // notifiy the sender that this message is sent 640 | close(job.r) 641 | } 642 | if err != nil { 643 | // This one is not interesting; it happens when clients 644 | // disappear before we send their acks. 645 | oe, isoe := err.(*net.OpError) 646 | if isoe && oe.Err.Error() == "use of closed network connection" { 647 | return 648 | } 649 | // In Go < 1.5, the error is not an OpError. 650 | if err.Error() == "use of closed network connection" { 651 | return 652 | } 653 | 654 | log.Print("writer: ", err) 655 | return 656 | } 657 | c.svr.stats.messageSend() 658 | 659 | if _, ok := job.m.(*proto.Disconnect); ok { 660 | log.Print("writer: sent disconnect message") 661 | return 662 | } 663 | } 664 | } 665 | 666 | // header is used to initialize a proto.Header when the zero value 667 | // is not correct. The zero value of proto.Header is 668 | // the equivalent of header(dupFalse, proto.QosAtMostOnce, retainFalse) 669 | // and is correct for most messages. 670 | func header(d dupFlag, q proto.QosLevel, r retainFlag) proto.Header { 671 | return proto.Header{ 672 | DupFlag: bool(d), QosLevel: q, Retain: bool(r), 673 | } 674 | } 675 | 676 | type retainFlag bool 677 | type dupFlag bool 678 | 679 | const ( 680 | retainFalse retainFlag = false 681 | retainTrue = true 682 | dupFalse dupFlag = false 683 | dupTrue = true 684 | ) 685 | 686 | func isWildcard(topic string) bool { 687 | if strings.Contains(topic, "#") || strings.Contains(topic, "+") { 688 | return true 689 | } 690 | return false 691 | } 692 | 693 | func (w wild) valid() bool { 694 | for i, part := range w.wild { 695 | // catch things like finance# 696 | if isWildcard(part) && len(part) != 1 { 697 | return false 698 | } 699 | // # can only occur as the last part 700 | if part == "#" && i != len(w.wild)-1 { 701 | return false 702 | } 703 | } 704 | return true 705 | } 706 | 707 | const clientQueueLength = 100 708 | 709 | // A ClientConn holds all the state associated with a connection 710 | // to an MQTT server. It should be allocated via NewClientConn. 711 | // Concurrent access to a ClientConn is NOT safe. 712 | type ClientConn struct { 713 | Dump bool // When true, dump the messages in and out. 714 | Incoming chan *proto.Publish // Incoming messages arrive on this channel. 715 | id uint16 // next MessageId 716 | out chan job 717 | conn net.Conn 718 | done chan struct{} // This channel will be readable once a Disconnect has been successfully sent and the connection is closed. 719 | connack chan *proto.ConnAck 720 | suback chan *proto.SubAck 721 | } 722 | 723 | // NewClientConn allocates a new ClientConn. 724 | func NewClientConn(c net.Conn) *ClientConn { 725 | cc := &ClientConn{ 726 | conn: c, 727 | id: 1, 728 | out: make(chan job, clientQueueLength), 729 | Incoming: make(chan *proto.Publish, clientQueueLength), 730 | done: make(chan struct{}), 731 | connack: make(chan *proto.ConnAck), 732 | suback: make(chan *proto.SubAck), 733 | } 734 | go cc.reader() 735 | go cc.writer() 736 | return cc 737 | } 738 | 739 | func (c *ClientConn) reader() { 740 | defer func() { 741 | // Cause the writer to exit. 742 | close(c.out) 743 | // Cause any goroutines waiting on messages to arrive to exit. 744 | close(c.Incoming) 745 | c.conn.Close() 746 | }() 747 | 748 | for { 749 | // TODO: timeout (first message and/or keepalives) 750 | m, err := proto.DecodeOneMessage(c.conn, nil) 751 | if err != nil { 752 | if err == io.EOF { 753 | return 754 | } 755 | if strings.HasSuffix(err.Error(), "use of closed network connection") { 756 | return 757 | } 758 | log.Print("cli reader: ", err) 759 | return 760 | } 761 | 762 | if c.Dump { 763 | log.Printf("dump in: %T", m) 764 | } 765 | 766 | switch m := m.(type) { 767 | case *proto.Publish: 768 | c.Incoming <- m 769 | case *proto.PubAck: 770 | // ignore these 771 | continue 772 | case *proto.ConnAck: 773 | c.connack <- m 774 | case *proto.SubAck: 775 | c.suback <- m 776 | case *proto.Disconnect: 777 | return 778 | default: 779 | log.Printf("cli reader: got msg type %T", m) 780 | } 781 | } 782 | } 783 | 784 | func (c *ClientConn) writer() { 785 | // Close connection on exit in order to cause reader to exit. 786 | defer func() { 787 | // Signal to Disconnect() that the message is on its way, or 788 | // that the connection is closing one way or the other... 789 | close(c.done) 790 | }() 791 | 792 | for job := range c.out { 793 | if c.Dump { 794 | log.Printf("dump out: %T", job.m) 795 | } 796 | 797 | // TODO: write timeout 798 | err := job.m.Encode(c.conn) 799 | if job.r != nil { 800 | close(job.r) 801 | } 802 | 803 | if err != nil { 804 | log.Print("cli writer: ", err) 805 | return 806 | } 807 | 808 | if _, ok := job.m.(*proto.Disconnect); ok { 809 | return 810 | } 811 | } 812 | } 813 | 814 | // Connect sends the CONNECT message to the server. If the ClientId is not already 815 | // set, use a default (a 63-bit decimal random number). The "clean session" 816 | // bit is always set. 817 | func (c *ClientConn) Connect(m *proto.Connect) error { 818 | // TODO: Keepalive timer 819 | if m.ClientId == "" { 820 | m.ClientId = fmt.Sprint(cliRand.Int63()) 821 | } 822 | m.ProtocolName = "MQIsdp" 823 | m.ProtocolVersion = 3 824 | 825 | if m.Username != "" { 826 | m.UsernameFlag = true 827 | m.PasswordFlag = true 828 | } 829 | 830 | if m.WillMessage != "" { 831 | m.WillFlag = true 832 | } 833 | 834 | c.sync(m) 835 | ack := <-c.connack 836 | return ConnectionErrors[ack.ReturnCode] 837 | } 838 | 839 | // ConnectionErrors is an array of errors corresponding to the 840 | // Connect return codes specified in the specification. 841 | var ConnectionErrors = [6]error{ 842 | nil, // Connection Accepted (not an error) 843 | errors.New("Connection Refused: unacceptable protocol version"), 844 | errors.New("Connection Refused: identifier rejected"), 845 | errors.New("Connection Refused: server unavailable"), 846 | errors.New("Connection Refused: bad user name or password"), 847 | errors.New("Connection Refused: not authorized"), 848 | } 849 | 850 | // Disconnect sends a DISCONNECT message to the server. This function 851 | // blocks until the disconnect message is actually sent, and the connection 852 | // is closed. 853 | func (c *ClientConn) Disconnect() { 854 | c.sync(&proto.Disconnect{}) 855 | <-c.done 856 | } 857 | 858 | func (c *ClientConn) nextid() uint16 { 859 | id := c.id 860 | c.id++ 861 | return id 862 | } 863 | 864 | // Subscribe subscribes this connection to a list of topics. Messages 865 | // will be delivered on the Incoming channel. 866 | func (c *ClientConn) Subscribe(tqs []proto.TopicQos) *proto.SubAck { 867 | c.sync(&proto.Subscribe{ 868 | Header: header(dupFalse, proto.QosAtLeastOnce, retainFalse), 869 | MessageId: c.nextid(), 870 | Topics: tqs, 871 | }) 872 | ack := <-c.suback 873 | return ack 874 | } 875 | 876 | // Publish publishes the given message to the MQTT server. 877 | // The QosLevel of the message must be QosAtLeastOnce for now. 878 | func (c *ClientConn) Publish(m *proto.Publish) { 879 | if m.QosLevel != proto.QosAtMostOnce { 880 | panic("unsupported QoS level") 881 | } 882 | m.MessageId = c.nextid() 883 | c.out <- job{m: m} 884 | } 885 | 886 | // sync sends a message and blocks until it was actually sent. 887 | func (c *ClientConn) sync(m proto.Message) { 888 | j := job{m: m, r: make(receipt)} 889 | c.out <- j 890 | <-j.r 891 | return 892 | } 893 | --------------------------------------------------------------------------------