diff --git a/README.md b/README.md index 55b58bc..5a69f35 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,16 @@ huaweicloud-iot-device-sdk-go提供设备接入华为云IoT物联网平台的Go版本的SDK,提供设备和平台之间通讯能力,以及设备服务、网关服务、OTA等高级服务。IoT设备开发者使用SDK可以大大简化开发复杂度,快速的接入平台。 - - ## 安装和构建 -安装和构建的过程取决于你是使用go的 [modules](https://golang.org/ref/mod)(推荐) 还是还是`GOPATH` +安装和构建的过程取决于你是使用go的 [modules](https://golang.org/ref/mod)(推荐) 还是还是`GOPATH` ### Modules -如果你使用 [modules](https://golang.org/ref/mod) 只需要导入包"github.com/ctlove0523/huaweicloud-iot-device-sdk-go"即可使用。当你使用go build命令构建项目时,依赖的包会自动被下载。注意使用go build命令构建时会自动下载最新版本,最新版本还没有达到release的标准可能存在一些尚未修复的bug。如果想使用稳定的发布版本可以从[release](https://github.com/ctlove0523/huaweicloud-iot-device-sdk-go/releases) 获取最新稳定的版本号,并在go.mod文件中指定版本号。 +如果你使用 [modules](https://golang.org/ref/mod) 只需要导入包"github.com/ctlove0523/huaweicloud-iot-device-sdk-go"即可使用。当你使用go +build命令构建项目时,依赖的包会自动被下载。注意使用go +build命令构建时会自动下载最新版本,最新版本还没有达到release的标准可能存在一些尚未修复的bug。如果想使用稳定的发布版本可以从[release](https://github.com/ctlove0523/huaweicloud-iot-device-sdk-go/releases) +获取最新稳定的版本号,并在go.mod文件中指定版本号。 ~~~go module example @@ -28,8 +29,6 @@ require github.com/ctlove0523/huaweicloud-iot-device-sdk-go v0.0.1-alpha go get github.com/ctlove0523/huaweicloud-iot-device-sdk-go ~~~ - - ## 使用API ### 创建设备并初始化 @@ -63,8 +62,6 @@ func main() { > iot-mqtts.cn-north-4.myhuaweicloud.com为华为IoT平台(基础班)在华为云北京四的访问端点,如果你购买了标准版或企业版,请将iot-mqtts.cn-north-4.myhuaweicloud.com更换为对应的MQTT协议接入端点。 - - ### 设备处理平台下发的命令 1、首先,在华为云IoT平台创建一个设备,设备的信息如下: @@ -143,8 +140,6 @@ func main() { } ~~~ - - > 设备支持的命令定义在产品中 ### 设备消息 @@ -232,8 +227,6 @@ func main() { } ~~~ - - ### 设备属性 1、首先,在华为云IoT平台创建一个设备,并在该设备下创建3个子设备,设备及子设备的信息如下: @@ -257,8 +250,6 @@ device.Init() fmt.Printf("device connected: %v\n", device.IsConnected()) ~~~ - - #### 设备属性上报 使用`ReportProperties(properties ServiceProperty) bool` 上报设备属性 @@ -282,8 +273,6 @@ services := iot.ServiceProperty{ device.ReportProperties(services) ~~~ - - #### 网关批量设备属性上报 使用`BatchReportSubDevicesProperties(service DevicesService)` 实现网关批量设备属性上报 @@ -312,8 +301,6 @@ device.BatchReportSubDevicesProperties(iot.DevicesService{ }) ~~~ - - #### 平台设置设备属性 使用`AddPropertiesSetHandler(handler DevicePropertiesSetHandler)` 注册平台设置设备属性handler,当接收到平台的命令时SDK回调。 @@ -327,8 +314,6 @@ device.AddPropertiesSetHandler(func(propertiesSetRequest iot.DevicePropertyDownR }) ~~~ - - #### 平台查询设备属性 使用`SetPropertyQueryHandler(handler DevicePropertyQueryHandler)`注册平台查询设备属性handler,当接收到平台的查询请求时SDK回调。 @@ -347,11 +332,10 @@ device.SetPropertyQueryHandler(func(query iot.DevicePropertyQueryRequest) iot.Se }) ~~~ - - #### 设备侧获取平台的设备影子数据 -使用`QueryDeviceShadow(query DevicePropertyQueryRequest, handler DevicePropertyQueryResponseHandler)` 可以查询平台的设备影子数据,当接收到平台的响应后SDK自动回调`DevicePropertyQueryResponseHandler`。 +使用`QueryDeviceShadow(query DevicePropertyQueryRequest, handler DevicePropertyQueryResponseHandler)` +可以查询平台的设备影子数据,当接收到平台的响应后SDK自动回调`DevicePropertyQueryResponseHandler`。 ~~~go // 设备查询设备影子数据 @@ -450,8 +434,6 @@ type DemoProperties struct { } ~~~ - - ## 报告bugs 如果你在使用过程中遇到任何问题或bugs,请通过issue的方式上报问题或bug,我们将会在第一时间内答复。上报问题或bugs时请尽量提供以下内容: diff --git a/constants.go b/constants.go index a099cc5..8aca16a 100644 --- a/constants.go +++ b/constants.go @@ -23,19 +23,26 @@ const ( PropertiesSetResponseTopic string = "$oc/devices/{device_id}/sys/properties/set/response/request_id=" // 平台查询设备属性 - PropertiesQueryRequestTopicName string = "propertiesQueryRequestTopicName" - PropertiesQueryRequestTopic string = "$oc/devices/{device_id}/sys/properties/get/#" + PropertiesQueryRequestTopicName string = "propertiesQueryRequestTopicName" + PropertiesQueryRequestTopic string = "$oc/devices/{device_id}/sys/properties/get/#" PropertiesQueryResponseTopicName string = "propertiesQueryResponseTopicName" PropertiesQueryResponseTopic string = "$oc/devices/{device_id}/sys/properties/get/response/request_id=" // 设备侧获取平台的设备影子数据 - DeviceShadowQueryRequestTopicName string = "deviceShadowQueryRequestTopicName" - DeviceShadowQueryRequestTopic string = "$oc/devices/{device_id}/sys/shadow/get/request_id=" + DeviceShadowQueryRequestTopicName string = "deviceShadowQueryRequestTopicName" + DeviceShadowQueryRequestTopic string = "$oc/devices/{device_id}/sys/shadow/get/request_id=" DeviceShadowQueryResponseTopicName string = "deviceShadowQueryResponseTopicName" - DeviceShadowQueryResponseTopic string = "$oc/devices/{device_id}/sys/shadow/get/response/#" + DeviceShadowQueryResponseTopic string = "$oc/devices/{device_id}/sys/shadow/get/response/#" // 网关批量上报子设备属性 GatewayBatchReportSubDeviceTopicName string = "gatewayBatchReportSubDeviceTopicName" - GatewayBatchReportSubDeviceTopic string = "$oc/devices/{device_id}/sys/gateway/sub_devices/properties/report" -) + GatewayBatchReportSubDeviceTopic string = "$oc/devices/{device_id}/sys/gateway/sub_devices/properties/report" + // 文件上传 + FileUploadUrlRequestTopicName string = "fileUploadUrlRequestTopicName" + FileUploadUrlRequestTopic string = "$oc/devices/{device_id}/sys/events/up" + FileUploadUrlResponseTopicName string = "fileUploadUrlResponseTopicName" + FileUploadUrlResponseTopic string = "$oc/devices/{device_id}/sys/events/down" + FileUploadResultTopicName string = "FileUploadResultTopic" + FileUploadResultTopic string = "$oc/devices/{device_id}/sys/events/up" +) diff --git a/handlers.go b/handlers.go index 441fd56..474cbbe 100644 --- a/handlers.go +++ b/handlers.go @@ -119,3 +119,90 @@ type DeviceService struct { DeviceId string `json:"device_id"` Services []ServicePropertyEntry `json:"services"` } + +// 文件上传下载管理 + +func CreateFileUploadResultResponse(filename string, result bool) FileUploadResultResponse { + code := 0 + if result { + code = 0 + } else { + code = 1 + } + paras := UploadFileResultResponseServiceEventParas{ + ObjectName: filename, + ResultCode: code, + } + + serviceEvent := UploadResultResponseServiceEvent{ + Paras: paras, + } + serviceEvent.ServiceId = "$file_manager" + serviceEvent.EventType = "upload_result_report" + serviceEvent.EventTime = GetEventTimeStamp() + + var services []UploadResultResponseServiceEvent + services = append(services, serviceEvent) + + response := FileUploadResultResponse{ + Services: services, + } + + return response +} + +type FileUploadUrlRequest struct { + ObjectDeviceId string `json:"object_device_id"` + Services []UploadRequestServiceEvent `json:"services"` +} + +type FileUploadUrlResponse struct { + ObjectDeviceId string `json:"object_device_id"` + Services []UploadResponseServiceEvent `json:"services"` +} + +type FileUploadResultResponse struct { + ObjectDeviceId string `json:"object_device_id"` + Services []UploadResultResponseServiceEvent `json:"services"` +} + +type BaseServiceEvent struct { + ServiceId string `json:"service_id"` + EventType string `json:"event_type"` + EventTime string `json:"event_time"` +} + +type UploadRequestServiceEvent struct { + BaseServiceEvent + Paras UploadRequestServiceEventParas `json:"paras"` +} + +type UploadResponseServiceEvent struct { + BaseServiceEvent + Paras UploadResponseServiceEventParas `json:"paras"` +} + +type UploadResultResponseServiceEvent struct { + BaseServiceEvent + Paras UploadFileResultResponseServiceEventParas `json:"paras"` +} + +type UploadRequestServiceEventParas struct { + FileName string `json:"file_name"` + FileAttributes interface{} `json:"file_attributes"` +} + +type UploadResponseServiceEventParas struct { + Url string `json:"url"` + BucketName string `json:"bucket_name"` + ObjectName string `json:"object_name"` + Expire int `json:"expire"` + FileAttributes interface{} `json:"file_attributes"` +} + +type UploadFileResultResponseServiceEventParas struct { + ObjectName string `json:"object_name"` + ResultCode int `json:"result_code"` + StatusCode int `json:"status_code"` + StatusDescription string `json:"status_description"` +} diff --git a/info.go b/info.go new file mode 100644 index 0000000..9d565f7 --- /dev/null +++ b/info.go @@ -0,0 +1,40 @@ +package iot + +import ( + "bufio" + "github.com/golang/glog" + "io" + "os" + "runtime" + "strings" +) + +func OsName() string { + return runtime.GOOS +} + +func SdkInfo() map[string]string { + f, err := os.Open("sdk_info") + if err != nil { + glog.Warning("read sdk info failed") + return map[string]string{} + } + + // 文件很小 + info := make(map[string]string) + buf := bufio.NewReader(f) + for { + b, _, err := buf.ReadLine() + if err != nil && err == io.EOF { + glog.Warningf("read sdk info failed or end") + break + } + line := string(b) + if len(line) != 0 { + parts := strings.Split(line, "=") + info[strings.Trim(parts[0], " ")] = strings.Trim(parts[1], " ") + } + } + + return info +} diff --git a/info_test.go b/info_test.go new file mode 100644 index 0000000..d5b6737 --- /dev/null +++ b/info_test.go @@ -0,0 +1,48 @@ +package iot + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + "testing" +) + +// 该测试用力仅能在Windows系统运行 +func TestOsName(t *testing.T) { + if !strings.Contains(OsName(), "windows") { + t.Errorf(`OsName must be windwos`) + } +} + +func TestVersion(t *testing.T) { + if SdkInfo()["sdk-version"] != "v0.0.1" { + t.Errorf("sdk version must be v0.0.1") + } + + if SdkInfo()["author"] != "chen tong" { + t.Errorf("sdk author must be chen tong") + } +} + +func TestCreateFileUploadResultResponse(t *testing.T) { + f, err := os.Open("sdk_info") + if err != nil { + fmt.Println(err.Error()) + } + + //建立缓冲区,把文件内容放到缓冲区中 + buf := bufio.NewReader(f) + for { + //遇到\n结束读取 + b, errR := buf.ReadBytes('\n') + if errR != nil { + if errR == io.EOF { + break + } + fmt.Println(errR.Error()) + } + fmt.Println(string(b)) + } +} diff --git a/iot_http_client.go b/iot_http_client.go new file mode 100644 index 0000000..9c1de1a --- /dev/null +++ b/iot_http_client.go @@ -0,0 +1,86 @@ +package iot + +import ( + "bytes" + "github.com/golang/glog" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/url" + "os" + "time" +) + +// 仅用于设备上传文件 +type HttpClient interface { + UploadFile(filename, uri string) bool +} + +type httpClient struct { + client *http.Client +} + +func (client *httpClient) UploadFile(filename, uri string) bool { + + bodyBuffer := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuffer) + + fileWriter, err := bodyWriter.CreateFormFile("files", filename) + if err != nil { + glog.Errorf("create form file failed %v", err) + return false + } + + file, err := os.Open(filename) + if err != nil { + glog.Errorf("open file failed %v", err) + return false + } + + defer file.Close() + + _, err = io.Copy(fileWriter, file) + if err != nil { + glog.Errorf("copy file to writer failed %v", err) + } + + //contentType := bodyWriter.FormDataContentType() + defer bodyWriter.Close() + + req, err := http.NewRequest("PUT", uri, bodyBuffer) + if err != nil { + glog.Errorf("create request filed %v", err) + } + + req.Header.Add("Content-Type", "text/plain") + + originalUri, err := url.ParseRequestURI(uri) + if err != nil { + glog.Errorf("parse request uri failed %v", err) + } + req.Header.Add("Host", originalUri.Host) + resp, _ := client.client.Do(req) + + defer resp.Body.Close() + + _, err = ioutil.ReadAll(resp.Body) + + return err == nil +} + +func CreateHttpClient() HttpClient { + tr := &http.Transport{ + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + DisableCompression: true, + } + innerClient := &http.Client{Transport: tr} + + httpClient := &httpClient{ + client: innerClient, + } + + return httpClient + +} diff --git a/iotdevice.go b/iotdevice.go index 71e6c23..288d693 100644 --- a/iotdevice.go +++ b/iotdevice.go @@ -2,6 +2,7 @@ package iot import ( "encoding/json" + "fmt" mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/golang/glog" "github.com/satori/go.uuid" @@ -20,6 +21,7 @@ type Device interface { AddCommandHandler(handler CommandHandler) AddPropertiesSetHandler(handler DevicePropertiesSetHandler) SetPropertyQueryHandler(handler DevicePropertyQueryHandler) + UploadFile(filename string) bool } type iotDevice struct { @@ -35,6 +37,75 @@ type iotDevice struct { topics map[string]string } +type FileAtt struct { + HashCode string `json:"hash_code"` + Size int `json:"size"` +} + +func (device *iotDevice) UploadFile(filename string) bool { + // first subscribe + uploadUrlChan := make(chan string) + + device.client.Subscribe(device.topics[FileUploadUrlResponseTopicName], 1, func(client mqtt.Client, message mqtt.Message) { + response := &FileUploadUrlResponse{} + if json.Unmarshal(message.Payload(), response) != nil { + glog.Errorf("unmarshal file upload response failed") + uploadUrlChan <- "" + } else { + uploadUrlChan <- response.Services[0].Paras.Url + } + }) + + // 构造获取文件上传URL的请求 + requestParas := UploadRequestServiceEventParas{ + FileName: filename, + } + + serviceEvent := UploadRequestServiceEvent{ + Paras: requestParas, + } + serviceEvent.ServiceId = "$file_manager" + serviceEvent.EventTime = GetEventTimeStamp() + serviceEvent.EventType = "get_upload_url" + + var services []UploadRequestServiceEvent + services = append(services, serviceEvent) + request := FileUploadUrlRequest{ + Services: services, + } + + if token := device.client.Publish(device.topics[FileUploadUrlRequestTopicName], 1, false, Interface2JsonString(request)); + token.Wait() && token.Error() != nil { + glog.Warningf("publish file upload request url failed") + return false + } + + upLoadUrl := <-uploadUrlChan + + if len(upLoadUrl) == 0 { + glog.Errorf("get file upload url failed") + return false + } + glog.Infof("file upload url is %s", upLoadUrl) + + filename = SmartFileName(filename) + uploadFlag := CreateHttpClient().UploadFile(filename, upLoadUrl) + if !uploadFlag { + glog.Errorf("upload file failed") + return false + } + + response := CreateFileUploadResultResponse(filename, uploadFlag) + + token := device.client.Publish(device.topics[FileUploadResultTopicName], 1, false, Interface2JsonString(response)) + if token.Wait() && token.Error() != nil { + glog.Error("report file upload file result failed") + return false + } + + return true +} + func (device *iotDevice) createMessageMqttHandler() func(client mqtt.Client, message mqtt.Message) { messageHandler := func(client mqtt.Client, message mqtt.Message) { msg := &Message{} @@ -141,6 +212,8 @@ func (device *iotDevice) Init() bool { options.SetClientID(assembleClientId(device)) options.SetUsername(device.Id) options.SetPassword(HmacSha256(device.Password, TimeStamp())) + fmt.Println(assembleClientId(device)) + fmt.Println(HmacSha256(device.Password, TimeStamp())) device.client = mqtt.NewClient(options) @@ -249,6 +322,9 @@ func CreateIotDevice(id, password, servers string) Device { device.topics[DeviceShadowQueryRequestTopicName] = FormatTopic(DeviceShadowQueryRequestTopic, id) device.topics[DeviceShadowQueryResponseTopicName] = FormatTopic(DeviceShadowQueryResponseTopic, id) device.topics[GatewayBatchReportSubDeviceTopicName] = FormatTopic(GatewayBatchReportSubDeviceTopic, id) + device.topics[FileUploadUrlRequestTopicName] = FormatTopic(FileUploadUrlRequestTopic, id) + device.topics[FileUploadUrlResponseTopicName] = FormatTopic(FileUploadUrlResponseTopic, id) + device.topics[FileUploadResultTopicName] = FormatTopic(FileUploadResultTopic, id) return device } @@ -264,7 +340,7 @@ func assembleClientId(device *iotDevice) string { func logFlush() { ticker := time.Tick(5 * time.Second) - for{ + for { select { case <-ticker: glog.Flush() diff --git a/samples/file/upload_file.go b/samples/file/upload_file.go new file mode 100644 index 0000000..3bb023f --- /dev/null +++ b/samples/file/upload_file.go @@ -0,0 +1,17 @@ +package main + +import ( + iot "github.com/ctlove0523/huaweicloud-iot-device-sdk-go" + "time" +) + +func main() { + //创建一个设备并初始化 + device := iot.CreateIotDevice("5fdb75cccbfe2f02ce81d4bf_go-mqtt", "123456789", "tcp://iot-mqtts.cn-north-4.myhuaweicloud.com:1883") + device.Init() + + device.UploadFile("D/software/mqttfx/chentong.txt") + + time.Sleep(time.Hour) + +} diff --git a/samples/loop_demo.go b/samples/loop_demo.go deleted file mode 100644 index 02114ed..0000000 --- a/samples/loop_demo.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -func main() { - ticker := time.Tick(5 * time.Second) - for ; ; { - select { - case <-ticker: - fmt.Println(time.Now().Second()) - - } - } -} diff --git a/samples/properties/device_properties.go b/samples/properties/device_properties.go index 12f1327..8cf5870 100644 --- a/samples/properties/device_properties.go +++ b/samples/properties/device_properties.go @@ -34,7 +34,7 @@ func main() { // 设备上报属性 props := iot.ServicePropertyEntry{ ServiceId: "value", - EventTime: iot.DataCollectionTime(), + EventTime: iot.GetEventTimeStamp(), Properties: DemoProperties{ Value: "chen tong", MsgType: "23", diff --git a/sdk_info b/sdk_info new file mode 100644 index 0000000..4dfffe2 --- /dev/null +++ b/sdk_info @@ -0,0 +1,2 @@ +author = chen tong +sdk-version = v0.0.1 diff --git a/util.go b/util.go index 6f5a31c..7cef580 100644 --- a/util.go +++ b/util.go @@ -20,7 +20,7 @@ func TimeStamp() string { // 设备采集数据UTC时间(格式:yyyyMMdd'T'HHmmss'Z'),如:20161219T114920Z。 //设备上报数据不带该参数或参数格式错误时,则数据上报时间以平台时间为准。 -func DataCollectionTime() string { +func GetEventTimeStamp() string { now := time.Now().UTC() return now.Format("20060102T150405Z") } @@ -32,6 +32,9 @@ func HmacSha256(data string, secret string) string { } func Interface2JsonString(v interface{}) string { + if v == nil { + return "" + } byteData, err := json.Marshal(v) if err != nil { return "" @@ -46,3 +49,15 @@ func GetTopicRequestId(topic string) string { func FormatTopic(topic, deviceId string) string { return strings.ReplaceAll(topic, "{device_id}", deviceId) } + +// 根据当前运行的操作系统重新修改文件路径以适配操作系统 +func SmartFileName(filename string) string { + // Windows操作系统适配 + if strings.Contains(OsName(), "windows") { + pathParts := strings.Split(filename, "/") + pathParts[0] = pathParts[0] + ":" + return strings.Join(pathParts, "\\\\") + } + + return filename +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..d08aba0 --- /dev/null +++ b/util_test.go @@ -0,0 +1,58 @@ +package iot + +import ( + "testing" +) + +func TestTimeStamp(t *testing.T) { + timeStamp := TimeStamp() + if len(timeStamp) != 10 { + t.Error(`Time Stamp length must be 10`) + } +} + +func TestDataCollectionTime(t *testing.T) { + if len(GetEventTimeStamp()) != 16 { + t.Errorf(`Data Collection Time length must be 16,but is %d`, len(GetEventTimeStamp())) + } +} + +func TestHmacSha256(t *testing.T) { + encodedPassword := "c0fefa1341fb0647290e93f641a9bcea74cd32111668cdc5f7418553640a55cc" + if HmacSha256("123456789", "202012222200") != encodedPassword { + t.Errorf("encoded password must be %s but is %s", encodedPassword, HmacSha256("123456789", "202012222200")) + } +} + +func TestInterface2JsonString(t *testing.T) { + if Interface2JsonString(nil) != "" { + t.Errorf("nill interface to json string must empty") + } +} + +func TestGetTopicRequestId(t *testing.T) { + topic := "$os/device/down/request=123456789" + if GetTopicRequestId(topic) != "123456789" { + t.Errorf("topic request id must be %s", "123456789") + } +} + +func TestFormatTopic(t *testing.T) { + topic := "$os/device/{device_id}/up" + deviceId := "123" + formatTopic := "$os/device/123/up" + if formatTopic != FormatTopic(topic, deviceId) { + t.Errorf("formated topic must be %s", formatTopic) + } + +} + +// 仅适用于windows系统 +func TestSmartFileName(t *testing.T) { + fileName := "D/go/sdk/test.log" + smartFileName := "D:\\\\go\\\\sdk\\\\test.log" + + if smartFileName != SmartFileName(fileName) { + t.Errorf("in windows file system,smart file name must be %s", smartFileName) + } +}