├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── adapter_endpoints.go ├── adapter_endpoints_test.go ├── adapter_info.go ├── adapter_info_test.go ├── adapter_initialise.go ├── adapter_initialise_test.go ├── adapter_reset.go ├── adapter_reset_test.go ├── device_state.go ├── events.go ├── events_test.go ├── go.mod ├── go.sum ├── joining.go ├── joining_test.go ├── messages.go ├── messages_test.go ├── network_manager.go ├── network_manager_test.go ├── node_address.go ├── node_address_test.go ├── node_bind.go ├── node_bind_test.go ├── node_description.go ├── node_description_test.go ├── node_endpoint_description.go ├── node_endpoint_description_test.go ├── node_endpoints.go ├── node_endpoints_test.go ├── node_receive_message.go ├── node_receive_message_test.go ├── node_remove.go ├── node_remove_test.go ├── node_request.go ├── node_request_test.go ├── node_send_message.go ├── node_send_message_test.go ├── node_table.go ├── node_table_test.go ├── node_unbind.go ├── node_unbind_test.go ├── nvram.go ├── nvram_test.go └── zstack.go /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push] 4 | jobs: 5 | 6 | build: 7 | name: Build 8 | runs-on: ubuntu-20.04 9 | steps: 10 | 11 | - name: Set up Go 1.22 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.22 15 | id: go 16 | 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v2 19 | 20 | - name: Get dependencies 21 | run: | 22 | go get -v -t -d ./... 23 | 24 | - name: Test 25 | run: go test -v . 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore IDEA project files. 2 | .idea 3 | *.iml -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shimmering Bee: Z-Stack 2 | 3 | [![license](https://img.shields.io/github/license/shimmeringbee/zstack.svg)](https://github.com/shimmeringbee/zstack/blob/master/LICENSE) 4 | [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg)](https://github.com/RichardLitt/standard-readme) 5 | [![Actions Status](https://github.com/shimmeringbee/zstack/workflows/test/badge.svg)](https://github.com/shimmeringbee/zstack/actions) 6 | 7 | > Implementation of a ZNP and support code designed to interface with Texas Instruments Z-Stack, written in Go. 8 | 9 | ## Table of Contents 10 | 11 | - [Background](#background) 12 | - [Install](#install) 13 | - [Usage](#usage) 14 | - [Maintainers](#maintainers) 15 | - [Contributing](#contributing) 16 | - [License](#license) 17 | 18 | ## Background 19 | 20 | Z-Stack is a Zigbee Stack made available by Texas Instruments for use on their CC 2.4Ghz SOC processors. This 21 | library implements a Zigbee Network Processor that is capable of controlling a Z-Stack implementation, specifically it 22 | supports the CC series of Zigbee sniffers flashed with the 23 | [zigbee2mqtt](https://www.zigbee2mqtt.io/getting_started/flashing_the_cc2531.html) Z-Stack coordinator firmware. 24 | 25 | More information about Z-Stack is available from [Texas Instruments](https://www.ti.com/tool/Z-STACK) directly or from 26 | [Z-Stack Developer's Guide](https://usermanual.wiki/Pdf/ZStack20Developers20Guide.1049398016/view). 27 | 28 | [Another implementation](https://github.com/dyrkin/znp-go/) of a Z-Stack compatible ZNP exists for Golang, it did [hold no license for a period](https://github.com/dyrkin/zigbee-steward/issues/1) 29 | and the author could not be contacted. This has been rectified, so it may be of interest you. This is a complete 30 | reimplementation of the library, however it is likely there will be strong coincidences due to Golang standards. 31 | 32 | ## Supported Devices 33 | 34 | The following chips and sticks are known to work, though it's likely others in the series will too: 35 | 36 | * CC253X 37 | * Cheap Zigbee Sniffers from AliExpress - CC2531 38 | * CC26X2R1 39 | * [Electrolama zig-a-sig-ah!](https://electrolama.com/projects/zig-a-zig-ah/) - CC2652R 40 | 41 | Huge thanks to @Koenkk for his work in providing Z-Stack firmware for these chips. You can [grab the firmware from GitHub](https://github.com/Koenkk/Z-Stack-firmware/). 42 | 43 | ## Install 44 | 45 | Add an import and most IDEs will `go get` automatically, if it doesn't `go build` will fetch. 46 | 47 | ```go 48 | import "github.com/shimmeringbee/zstack" 49 | ``` 50 | 51 | ## Usage 52 | 53 | **This libraries API is unstable and should not yet be relied upon.** 54 | 55 | ### Open Serial Connection and Start ZStack 56 | 57 | ```go 58 | /* Obtain a ReadWriter UART interface to CC253X */ 59 | serialPort := 60 | 61 | /* Construct node table, cache of network nodes. */ 62 | t := zstack.NewNodeTable() 63 | 64 | /* Create a new ZStack struct. */ 65 | z := zstack.New(serialPort, t) 66 | 67 | /* Generate random Zigbee network, on default channel (15) */ 68 | netCfg, _ := zigbee.GenerateNetworkConfiguration() 69 | 70 | /* Obtain context for timeout of initialisation. */ 71 | ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Minute) 72 | defer cancel() 73 | 74 | /* Initialise ZStack and CC253X */) 75 | err = z.Initialise(ctx, nc) 76 | ``` 77 | 78 | ### Handle Events 79 | 80 | **It is critical that this is handled until you wish to stop the Z-Stack instance.** 81 | 82 | ```go 83 | for { 84 | ctx := context.Background() 85 | event, err := z.ReadEvent(ctx) 86 | 87 | if err != nil { 88 | return 89 | } 90 | 91 | switch e := event.(type) { 92 | case zigbee.NodeJoinEvent: 93 | log.Printf("join: %v\n", e.Node) 94 | go exploreDevice(z, e.Node) 95 | case zigbee.NodeLeaveEvent: 96 | log.Printf("leave: %v\n", e.Node) 97 | case zigbee.NodeUpdateEvent: 98 | log.Printf("update: %v\n", e.Node) 99 | case zigbee.NodeIncomingMessageEvent: 100 | log.Printf("message: %v\n", e) 101 | } 102 | } 103 | ``` 104 | 105 | ### Permit Joins 106 | 107 | ```go 108 | err := z.PermitJoin(ctx, true) 109 | ``` 110 | 111 | ### Deny Joins 112 | 113 | ```go 114 | err := z.DenyJoin(ctx) 115 | ``` 116 | 117 | ### Query Device For Details 118 | 119 | ```go 120 | func exploreDevice(z *zstack.ZStack, node zigbee.Node) { 121 | log.Printf("node %v: querying", node.IEEEAddress) 122 | 123 | ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Minute) 124 | defer cancel() 125 | 126 | descriptor, err := z.QueryNodeDescription(ctx, node.IEEEAddress) 127 | 128 | if err != nil { 129 | log.Printf("failed to get node descriptor: %v", err) 130 | return 131 | } 132 | 133 | log.Printf("node %v: descriptor: %+v", node.IEEEAddress, descriptor) 134 | 135 | endpoints, err := z.QueryNodeEndpoints(ctx, node.IEEEAddress) 136 | 137 | if err != nil { 138 | log.Printf("failed to get node endpoints: %v", err) 139 | return 140 | } 141 | 142 | log.Printf("node %v: endpoints: %+v", node.IEEEAddress, endpoints) 143 | 144 | for _, endpoint := range endpoints { 145 | endpointDes, err := z.QueryNodeEndpointDescription(ctx, node.IEEEAddress, endpoint) 146 | 147 | if err != nil { 148 | log.Printf("failed to get node endpoint description: %v / %d", err, endpoint) 149 | } else { 150 | log.Printf("node %v: endpoint: %d desc: %+v", node.IEEEAddress, endpoint, endpointDes) 151 | } 152 | } 153 | } 154 | ``` 155 | 156 | ### Node Table Cache 157 | 158 | `zstack` requires a `NodeTable` structure to cache a devices IEEE address to its Zibgee network address. A design 159 | decision for `zstack` was that all operations would reference the IEEE address. This cache must be persisted between 160 | program runs as the coordinator hardware does not retain this information between restarts. 161 | 162 | ```go 163 | // Create new table 164 | nodeTable := NewNodeTable() 165 | 166 | // Dump current content 167 | nodes := nodeTable.Nodes() 168 | 169 | // Load previous content - this should be done before starting ZStack. 170 | nodeTable.Load(nodes) 171 | ``` 172 | 173 | ### ZCL 174 | 175 | To handle ZCL messages you must handle `zigbee.NodeIncomingMessageEvent` messages and process the ZCL payload with the ZCL library, responses can be sent with `z.SendNodeMessage`. 176 | 177 | ## Maintainers 178 | 179 | [@pwood](https://github.com/pwood) 180 | 181 | ## Contributing 182 | 183 | Feel free to dive in! [Open an issue](https://github.com/shimmeringbee/zstack/issues/new) or submit PRs. 184 | 185 | All Shimmering Bee projects follow the [Contributor Covenant](https://shimmeringbee.io/docs/code_of_conduct/) Code of Conduct. 186 | 187 | ## License 188 | 189 | Copyright 2019-2020 Shimmering Bee Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities, consumers are expected to track the latest version of the library. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | For low risk security vulnerabilities CVSS 3.1 scores of < 5.0, you may log the issue via the GitHub tracker. Otherwise 10 | and for all other vulnerabilities, please report (suspected) security vulnerabilities to *security@shimmeringbee.io*. 11 | We are an open source volunteer project, we aim to respond to you within 72 hours. 12 | If the issue is confirmed, we will release a patch as soon as possible depending on complexity. -------------------------------------------------------------------------------- /adapter_endpoints.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) RegisterAdapterEndpoint(ctx context.Context, endpoint zigbee.Endpoint, appProfileId zigbee.ProfileID, appDeviceId uint16, appDeviceVersion uint8, inClusters []zigbee.ClusterID, outClusters []zigbee.ClusterID) error { 10 | if err := z.sem.Acquire(ctx, 1); err != nil { 11 | return fmt.Errorf("failed to acquire semaphore: %w", err) 12 | } 13 | defer z.sem.Release(1) 14 | 15 | request := AFRegister{ 16 | Endpoint: endpoint, 17 | AppProfileId: appProfileId, 18 | AppDeviceId: appDeviceId, 19 | AppDeviceVersion: appDeviceVersion, 20 | LatencyReq: 0x00, // No latency, no other valid option for Zigbee 21 | AppInClusters: inClusters, 22 | AppOutClusters: outClusters, 23 | } 24 | 25 | resp := AFRegisterReply{} 26 | 27 | if err := z.requestResponder.RequestResponse(ctx, request, &resp); err != nil { 28 | return err 29 | } 30 | 31 | if resp.Status != ZSuccess { 32 | return ErrorZFailure 33 | } 34 | 35 | return nil 36 | } 37 | 38 | type AFRegister struct { 39 | Endpoint zigbee.Endpoint 40 | AppProfileId zigbee.ProfileID 41 | AppDeviceId uint16 42 | AppDeviceVersion uint8 43 | LatencyReq uint8 44 | AppInClusters []zigbee.ClusterID `bcsliceprefix:"8"` 45 | AppOutClusters []zigbee.ClusterID `bcsliceprefix:"8"` 46 | } 47 | 48 | const AFRegisterID uint8 = 0x00 49 | 50 | type AFRegisterReply GenericZStackStatus 51 | 52 | const AFRegisterReplyID uint8 = 0x00 53 | -------------------------------------------------------------------------------- /adapter_endpoints_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_RegisterAdapterEndpoint(t *testing.T) { 17 | t.Run("registers the endpoint", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | zstack := New(unpiMock, memory.New()) 23 | zstack.sem = semaphore.NewWeighted(8) 24 | defer unpiMock.Stop() 25 | 26 | c := unpiMock.On(SREQ, AF, AFRegisterID).Return(Frame{ 27 | MessageType: SRSP, 28 | Subsystem: AF, 29 | CommandID: AFRegisterReplyID, 30 | Payload: []byte{0x00}, 31 | }) 32 | 33 | err := zstack.RegisterAdapterEndpoint(ctx, 0x01, 0x0104, 0x0001, 0x01, []zigbee.ClusterID{0x0001}, []zigbee.ClusterID{0x0002}) 34 | assert.NoError(t, err) 35 | 36 | unpiMock.AssertCalls(t) 37 | 38 | frame := c.CapturedCalls[0].Frame 39 | 40 | assert.Equal(t, []byte{0x01, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x02, 0x00}, frame.Payload) 41 | }) 42 | 43 | t.Run("returns an error if the query fails", func(t *testing.T) { 44 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 45 | defer cancel() 46 | 47 | unpiMock := unpiTest.NewMockAdapter() 48 | zstack := New(unpiMock, memory.New()) 49 | zstack.sem = semaphore.NewWeighted(8) 50 | defer unpiMock.Stop() 51 | 52 | unpiMock.On(SREQ, AF, AFRegisterID).Return(Frame{ 53 | MessageType: SRSP, 54 | Subsystem: AF, 55 | CommandID: AFRegisterReplyID, 56 | Payload: []byte{0x01}, 57 | }) 58 | 59 | err := zstack.RegisterAdapterEndpoint(ctx, 0x01, 0x0104, 0x0001, 0x01, []zigbee.ClusterID{0x0001}, []zigbee.ClusterID{0x0002}) 60 | assert.Error(t, err) 61 | assert.Equal(t, ErrorZFailure, err) 62 | 63 | unpiMock.AssertCalls(t) 64 | }) 65 | } 66 | 67 | func Test_EndpointRegisterMessages(t *testing.T) { 68 | t.Run("verify AFRegister marshals", func(t *testing.T) { 69 | req := AFRegister{ 70 | Endpoint: 1, 71 | AppProfileId: 2, 72 | AppDeviceId: 3, 73 | AppDeviceVersion: 4, 74 | LatencyReq: 5, 75 | AppInClusters: []zigbee.ClusterID{0x10}, 76 | AppOutClusters: []zigbee.ClusterID{0x20}, 77 | } 78 | 79 | data, err := bytecodec.Marshal(req) 80 | 81 | assert.NoError(t, err) 82 | assert.Equal(t, []byte{0x01, 0x02, 0x00, 0x03, 0x00, 0x04, 0x05, 0x01, 0x10, 0x00, 0x01, 0x20, 0x00}, data) 83 | }) 84 | 85 | t.Run("verify AFRegisterReply marshals", func(t *testing.T) { 86 | req := AFRegisterReply{ 87 | Status: 1, 88 | } 89 | 90 | data, err := bytecodec.Marshal(req) 91 | 92 | assert.NoError(t, err) 93 | assert.Equal(t, []byte{0x01}, data) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /adapter_info.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) AdapterNode() zigbee.Node { 10 | return zigbee.Node{ 11 | IEEEAddress: z.NetworkProperties.IEEEAddress, 12 | NetworkAddress: z.NetworkProperties.NetworkAddress, 13 | LogicalType: zigbee.Coordinator, 14 | } 15 | } 16 | 17 | func (z *ZStack) GetAdapterIEEEAddress(ctx context.Context) (zigbee.IEEEAddress, error) { 18 | data, err := z.getAddressInfo(ctx) 19 | ieeeAddress := data.IEEEAddress 20 | 21 | return ieeeAddress, err 22 | } 23 | 24 | func (z *ZStack) GetAdapterNetworkAddress(ctx context.Context) (zigbee.NetworkAddress, error) { 25 | data, err := z.getAddressInfo(ctx) 26 | 27 | networkAddress := data.NetworkAddress 28 | return networkAddress, err 29 | } 30 | 31 | func (z *ZStack) getAddressInfo(ctx context.Context) (UtilGetDeviceInfoRequestReply, error) { 32 | resp := UtilGetDeviceInfoRequestReply{} 33 | 34 | if err := z.sem.Acquire(ctx, 1); err != nil { 35 | return resp, fmt.Errorf("failed to acquire semaphore: %w", err) 36 | } 37 | defer z.sem.Release(1) 38 | 39 | err := z.requestResponder.RequestResponse(ctx, UtilGetDeviceInfoRequest{}, &resp) 40 | return resp, err 41 | } 42 | 43 | type UtilGetDeviceInfoRequest struct{} 44 | 45 | const UtilGetDeviceInfoRequestID uint8 = 0x00 46 | 47 | type UtilGetDeviceInfoRequestReply struct { 48 | Status uint8 49 | IEEEAddress zigbee.IEEEAddress 50 | NetworkAddress zigbee.NetworkAddress 51 | } 52 | 53 | const UtilGetDeviceInfoRequestReplyID uint8 = 0x00 54 | -------------------------------------------------------------------------------- /adapter_info_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_GetAdapterIEEEAddress(t *testing.T) { 17 | t.Run("gets the IEEE address", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | zstack := New(unpiMock, memory.New()) 23 | zstack.sem = semaphore.NewWeighted(8) 24 | defer unpiMock.Stop() 25 | 26 | unpiMock.On(SREQ, UTIL, UtilGetDeviceInfoRequestID).Return(Frame{ 27 | MessageType: SRSP, 28 | Subsystem: UTIL, 29 | CommandID: UtilGetDeviceInfoRequestReplyID, 30 | Payload: []byte{0x00, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x12, 0x11}, 31 | }) 32 | 33 | address, err := zstack.GetAdapterIEEEAddress(ctx) 34 | assert.NoError(t, err) 35 | assert.Equal(t, zigbee.IEEEAddress(0x0203040506070809), address) 36 | 37 | unpiMock.AssertCalls(t) 38 | }) 39 | } 40 | 41 | func Test_GetAdapterNetworkAddress(t *testing.T) { 42 | t.Run("gets the network address", func(t *testing.T) { 43 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 44 | defer cancel() 45 | 46 | unpiMock := unpiTest.NewMockAdapter() 47 | zstack := New(unpiMock, memory.New()) 48 | zstack.sem = semaphore.NewWeighted(8) 49 | defer unpiMock.Stop() 50 | 51 | unpiMock.On(SREQ, UTIL, UtilGetDeviceInfoRequestID).Return(Frame{ 52 | MessageType: SRSP, 53 | Subsystem: UTIL, 54 | CommandID: UtilGetDeviceInfoRequestReplyID, 55 | Payload: []byte{0x00, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x12, 0x11}, 56 | }) 57 | 58 | address, err := zstack.GetAdapterNetworkAddress(ctx) 59 | assert.NoError(t, err) 60 | assert.Equal(t, zigbee.NetworkAddress(0x1112), address) 61 | 62 | unpiMock.AssertCalls(t) 63 | }) 64 | } 65 | 66 | func Test_UtilGetDeviceInfoStructs(t *testing.T) { 67 | t.Run("UtilGetDeviceInfoRequestReply", func(t *testing.T) { 68 | s := UtilGetDeviceInfoRequestReply{ 69 | Status: 0x01, 70 | IEEEAddress: 0x0203040506070809, 71 | NetworkAddress: 0x1112, 72 | } 73 | 74 | actualBytes, err := bytecodec.Marshal(s) 75 | 76 | expectedBytes := []byte{0x01, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x12, 0x11} 77 | 78 | assert.NoError(t, err) 79 | assert.Equal(t, expectedBytes, actualBytes) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /adapter_initialise.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/shimmeringbee/logwrap" 8 | "github.com/shimmeringbee/retry" 9 | "github.com/shimmeringbee/zigbee" 10 | "golang.org/x/sync/semaphore" 11 | "reflect" 12 | ) 13 | 14 | func (z *ZStack) Initialise(pctx context.Context, nc zigbee.NetworkConfiguration) error { 15 | z.NetworkProperties.PANID = nc.PANID 16 | z.NetworkProperties.ExtendedPANID = nc.ExtendedPANID 17 | z.NetworkProperties.NetworkKey = nc.NetworkKey 18 | z.NetworkProperties.Channel = nc.Channel 19 | 20 | ctx, segmentEnd := z.logger.Segment(pctx, "Adapter Initialise.") 21 | defer segmentEnd() 22 | 23 | z.logger.LogInfo(ctx, "Restarting adapter.") 24 | version, err := z.waitForAdapterReset(ctx) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if version.IsV3() { 30 | z.sem = semaphore.NewWeighted(16) 31 | } else { 32 | z.sem = semaphore.NewWeighted(2) 33 | } 34 | 35 | z.logger.LogInfo(ctx, "Verifying existing network configuration.") 36 | if valid, err := z.verifyAdapterNetworkConfig(ctx, version); err != nil { 37 | return err 38 | } else if !valid { 39 | z.logger.LogWarn(ctx, "Adapter network configuration is invalid, resetting adapter.") 40 | if err := z.wipeAdapter(ctx); err != nil { 41 | return err 42 | } 43 | 44 | z.logger.LogInfo(ctx, "Setting adapter to coordinator.") 45 | if err := z.makeCoordinator(ctx); err != nil { 46 | return err 47 | } 48 | 49 | z.logger.LogInfo(ctx, "Configuring adapter.") 50 | if err := z.configureNetwork(ctx, version); err != nil { 51 | return err 52 | } 53 | } 54 | 55 | z.logger.LogInfo(ctx, "Starting Zigbee stack.") 56 | if err := z.startZigbeeStack(ctx, version); err != nil { 57 | return err 58 | } 59 | 60 | z.logger.LogInfo(ctx, "Fetching adapter IEEE and Network addresses.") 61 | if err := z.retrieveAdapterAddresses(ctx); err != nil { 62 | return err 63 | } 64 | 65 | z.logger.LogInfo(ctx, "Enforcing denial of network joins.") 66 | if err := z.DenyJoin(ctx); err != nil { 67 | return err 68 | } 69 | 70 | z.startNetworkManager() 71 | z.startMessageReceiver() 72 | 73 | return nil 74 | } 75 | 76 | func (z *ZStack) waitForAdapterReset(ctx context.Context) (Version, error) { 77 | retVersion := Version{} 78 | 79 | err := retry.Retry(ctx, DefaultZStackTimeout, 18, func(invokeCtx context.Context) error { 80 | version, err := z.resetAdapter(invokeCtx, Soft) 81 | retVersion = version 82 | return err 83 | }) 84 | 85 | return retVersion, err 86 | } 87 | 88 | func (z *ZStack) verifyAdapterNetworkConfig(ctx context.Context, version Version) (bool, error) { 89 | configToVerify := []interface{}{ 90 | &ZCDNVLogicalType{LogicalType: zigbee.Coordinator}, 91 | &ZCDNVPANID{PANID: z.NetworkProperties.PANID}, 92 | &ZCDNVExtPANID{ExtendedPANID: z.NetworkProperties.ExtendedPANID}, 93 | &ZCDNVChanList{Channels: channelToBits(z.NetworkProperties.Channel)}, 94 | } 95 | 96 | for _, expectedConfig := range configToVerify { 97 | configType := reflect.TypeOf(expectedConfig).Elem() 98 | actualConfig := reflect.New(configType).Interface() 99 | 100 | if err := z.readNVRAM(ctx, actualConfig); err != nil { 101 | return false, err 102 | } 103 | 104 | if !reflect.DeepEqual(expectedConfig, actualConfig) { 105 | return false, nil 106 | } 107 | } 108 | 109 | return true, nil 110 | } 111 | 112 | func (z *ZStack) wipeAdapter(ctx context.Context) error { 113 | return retryFunctions(ctx, []func(context.Context) error{ 114 | func(invokeCtx context.Context) error { 115 | return z.writeNVRAM(invokeCtx, ZCDNVStartUpOption{StartOption: 0x03}) 116 | }, 117 | func(invokeCtx context.Context) error { 118 | _, err := z.resetAdapter(invokeCtx, Soft) 119 | return err 120 | }, 121 | }) 122 | } 123 | 124 | func (z *ZStack) makeCoordinator(ctx context.Context) error { 125 | return retryFunctions(ctx, []func(context.Context) error{ 126 | func(invokeCtx context.Context) error { 127 | return z.writeNVRAM(invokeCtx, ZCDNVLogicalType{LogicalType: zigbee.Coordinator}) 128 | }, 129 | func(invokeCtx context.Context) error { 130 | _, err := z.resetAdapter(invokeCtx, Soft) 131 | return err 132 | }, 133 | }) 134 | } 135 | 136 | func (z *ZStack) configureNetwork(ctx context.Context, version Version) error { 137 | channelBits := channelToBits(z.NetworkProperties.Channel) 138 | 139 | if err := retryFunctions(ctx, []func(context.Context) error{ 140 | func(invokeCtx context.Context) error { 141 | z.logger.LogDebug(ctx, "Adapter Initialisation: Enabling preconfigured keys.") 142 | return z.writeNVRAM(invokeCtx, ZCDNVPreCfgKeysEnable{Enabled: 1}) 143 | }, 144 | func(invokeCtx context.Context) error { 145 | z.logger.LogDebug(ctx, "Adapter Initialisation: Configuring network key.") 146 | return z.writeNVRAM(invokeCtx, ZCDNVPreCfgKey{NetworkKey: z.NetworkProperties.NetworkKey}) 147 | }, 148 | func(invokeCtx context.Context) error { 149 | z.logger.LogDebug(ctx, "Adapter Initialisation: Enable ZDO callbacks.") 150 | return z.writeNVRAM(invokeCtx, ZCDNVZDODirectCB{Enabled: 1}) 151 | }, 152 | func(invokeCtx context.Context) error { 153 | z.logger.LogDebug(ctx, "Adapter Initialisation: Configuring network channel.") 154 | return z.writeNVRAM(invokeCtx, ZCDNVChanList{Channels: channelBits}) 155 | }, 156 | func(invokeCtx context.Context) error { 157 | z.logger.LogDebug(ctx, "Adapter Initialisation: Configuring network PANID.") 158 | return z.writeNVRAM(invokeCtx, ZCDNVPANID{PANID: z.NetworkProperties.PANID}) 159 | }, 160 | func(invokeCtx context.Context) error { 161 | z.logger.LogDebug(ctx, "Adapter Initialisation: Configuring network extended PANID.") 162 | return z.writeNVRAM(invokeCtx, ZCDNVExtPANID{ExtendedPANID: z.NetworkProperties.ExtendedPANID}) 163 | }, 164 | }); err != nil { 165 | return err 166 | } 167 | 168 | if !version.IsV3() { 169 | z.logger.LogDebug(ctx, "Adapter Initialisation: Not Version 3.X.X.") 170 | /* Less than Z-Stack 3.X.X requires the Trust Centre key to be loaded. */ 171 | return retryFunctions(ctx, []func(context.Context) error{ 172 | func(invokeCtx context.Context) error { 173 | z.logger.LogDebug(ctx, "Adapter Initialisation: Enable default trust center.") 174 | return z.writeNVRAM(invokeCtx, ZCDNVUseDefaultTCLK{Enabled: 1}) 175 | }, 176 | func(invokeCtx context.Context) error { 177 | z.logger.LogDebug(ctx, "Adapter Initialisation: Configuring ZLL trust center key.") 178 | return z.writeNVRAM(invokeCtx, ZCDNVTCLKTableStart{ 179 | Address: zigbee.IEEEAddress(0xffffffffffffffff), 180 | NetworkKey: zigbee.TCLinkKey, 181 | TXFrameCounter: 0, 182 | RXFrameCounter: 0, 183 | }) 184 | }, 185 | }) 186 | } else { 187 | /* Z-Stack 3.X.X requires configuration of Base Device Behaviour. */ 188 | z.logger.LogDebug(ctx, "Adapter Initialisation: Version 3.X.X.") 189 | if err := retryFunctions(ctx, []func(context.Context) error{ 190 | func(invokeCtx context.Context) error { 191 | z.logger.LogDebug(ctx, "Adapter Initialisation: Configure primary channel.") 192 | return z.requestResponder.RequestResponse(ctx, APPCNFBDBSetChannelRequest{IsPrimary: true, Channel: channelBits}, &APPCNFBDBSetChannelRequestReply{}) 193 | }, 194 | func(invokeCtx context.Context) error { 195 | z.logger.LogDebug(ctx, "Adapter Initialisation: Configure secondary channels.") 196 | return z.requestResponder.RequestResponse(ctx, APPCNFBDBSetChannelRequest{IsPrimary: false, Channel: [4]byte{}}, &APPCNFBDBSetChannelRequestReply{}) 197 | }, 198 | func(invokeCtx context.Context) error { 199 | z.logger.LogDebug(ctx, "Adapter Initialisation: Request commissioning.") 200 | return z.requestResponder.RequestResponse(ctx, APPCNFBDBStartCommissioningRequest{Mode: 0x04}, &APPCNFBDBStartCommissioningRequestReply{}) 201 | }, 202 | }); err != nil { 203 | return err 204 | } 205 | 206 | z.logger.LogDebug(ctx, "Adapter Initialisation: Waiting for coordinator to start.") 207 | if err := z.waitForCoordinatorStart(ctx); err != nil { 208 | return err 209 | } 210 | 211 | return retryFunctions(ctx, []func(context.Context) error{ 212 | func(invokeCtx context.Context) error { 213 | z.logger.LogDebug(ctx, "Adapter Initialisation: Waiting for commissioning to complete.") 214 | return z.requestResponder.RequestResponse(ctx, APPCNFBDBStartCommissioningRequest{Mode: 0x02}, &APPCNFBDBStartCommissioningRequestReply{}) 215 | }, 216 | }) 217 | } 218 | } 219 | 220 | func (z *ZStack) retrieveAdapterAddresses(ctx context.Context) error { 221 | return retryFunctions(ctx, []func(context.Context) error{ 222 | func(invokeCtx context.Context) error { 223 | if address, err := z.GetAdapterIEEEAddress(ctx); err != nil { 224 | return err 225 | } else { 226 | z.NetworkProperties.IEEEAddress = address 227 | return nil 228 | } 229 | }, 230 | func(ctx context.Context) error { 231 | if address, err := z.GetAdapterNetworkAddress(ctx); err != nil { 232 | return err 233 | } else { 234 | z.NetworkProperties.NetworkAddress = address 235 | return nil 236 | } 237 | }, 238 | }) 239 | } 240 | 241 | func (z *ZStack) startZigbeeStack(ctx context.Context, version Version) error { 242 | if err := retry.Retry(ctx, DefaultZStackTimeout, DefaultZStackRetries, func(invokeCtx context.Context) error { 243 | return z.requestResponder.RequestResponse(invokeCtx, ZDOStartUpFromAppRequest{StartDelay: 100}, &ZDOStartUpFromAppRequestReply{}) 244 | }); err != nil { 245 | return err 246 | } 247 | 248 | if version.IsV3() { 249 | return nil 250 | } 251 | 252 | ch := make(chan bool, 1) 253 | defer close(ch) 254 | 255 | err, cancel := z.subscriber.Subscribe(&ZDOStateChangeInd{}, func(v interface{}) { 256 | stateChange := v.(*ZDOStateChangeInd) 257 | z.logger.LogDebug(ctx, "Waiting for zigbee stack to start, state change.", logwrap.Datum("State", stateChange.State), logwrap.Datum("DesiredState", DeviceZBCoordinator)) 258 | if stateChange.State == DeviceZBCoordinator { 259 | ch <- true 260 | } 261 | }) 262 | defer cancel() 263 | 264 | if err != nil { 265 | return err 266 | } 267 | 268 | select { 269 | case <-ch: 270 | return nil 271 | case <-ctx.Done(): 272 | return errors.New("context expired while waiting for adapter start up") 273 | } 274 | } 275 | 276 | func (z *ZStack) waitForCoordinatorStart(ctx context.Context) error { 277 | ch := make(chan bool, 1) 278 | defer close(ch) 279 | 280 | err, cancel := z.subscriber.Subscribe(&ZDOStateChangeInd{}, func(v interface{}) { 281 | stateChange := v.(*ZDOStateChangeInd) 282 | z.logger.LogDebug(ctx, "Waiting for coordinator start, state change.", logwrap.Datum("State", stateChange.State), logwrap.Datum("DesiredState", DeviceZBCoordinator)) 283 | if stateChange.State == DeviceZBCoordinator { 284 | ch <- true 285 | } 286 | }) 287 | defer cancel() 288 | 289 | if err != nil { 290 | return err 291 | } 292 | 293 | select { 294 | case <-ch: 295 | return nil 296 | case <-ctx.Done(): 297 | return errors.New("context expired while waiting for adapter start up") 298 | } 299 | } 300 | 301 | func retryFunctions(ctx context.Context, funcs []func(context.Context) error) error { 302 | for _, f := range funcs { 303 | if err := retry.Retry(ctx, DefaultZStackTimeout, DefaultZStackRetries, f); err != nil { 304 | return fmt.Errorf("failed during configuration and initialisation: %w", err) 305 | } 306 | } 307 | 308 | return nil 309 | } 310 | 311 | func channelToBits(channel uint8) [4]byte { 312 | channelBits := 1 << channel 313 | 314 | channelBytes := [4]byte{} 315 | channelBytes[0] = byte((channelBits >> 0) & 0xff) 316 | channelBytes[1] = byte((channelBits >> 8) & 0xff) 317 | channelBytes[2] = byte((channelBits >> 16) & 0xff) 318 | channelBytes[3] = byte((channelBits >> 24) & 0xff) 319 | 320 | return channelBytes 321 | } 322 | 323 | type ZBStartStatus uint8 324 | 325 | const ( 326 | ZBSuccess ZBStartStatus = 0x00 327 | ZBInit ZBStartStatus = 0x22 328 | ) 329 | 330 | type ZDOState uint8 331 | 332 | const ( 333 | DeviceCoordinatorStarting ZDOState = 0x08 334 | DeviceZBCoordinator ZDOState = 0x09 335 | ) 336 | 337 | type ZDOStateChangeInd struct { 338 | State ZDOState 339 | } 340 | 341 | const ZDOStateChangeIndID uint8 = 0xc0 342 | 343 | type APPCNFBDBStartCommissioningRequest struct { 344 | Mode uint8 345 | } 346 | 347 | const APPCNFBDBStartCommissioningRequestID uint8 = 0x05 348 | 349 | type APPCNFBDBStartCommissioningRequestReply GenericZStackStatus 350 | 351 | const APPCNFBDBStartCommissioningRequestReplyID uint8 = 0x05 352 | 353 | type APPCNFBDBSetChannelRequest struct { 354 | IsPrimary bool `bcwidth:"8"` 355 | Channel [4]byte 356 | } 357 | 358 | const APPCNFBDBSetChannelRequestID uint8 = 0x08 359 | 360 | type APPCNFBDBSetChannelRequestReply GenericZStackStatus 361 | 362 | const APPCNFBDBSetChannelRequestReplyID uint8 = 0x08 363 | 364 | type ZDOStartUpFromAppRequest struct { 365 | StartDelay uint16 366 | } 367 | 368 | const ZDOStartUpFromAppRequestId uint8 = 0x40 369 | 370 | type ZDOStartUpFromAppRequestReply struct { 371 | Status uint8 372 | } 373 | 374 | const ZDOStartUpFromAppRequestReplyID uint8 = 0x40 375 | -------------------------------------------------------------------------------- /adapter_reset.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import "context" 4 | 5 | func (z *ZStack) resetAdapter(ctx context.Context, resetType ResetType) (Version, error) { 6 | resetInd := &SysResetInd{} 7 | err := z.requestResponder.RequestResponse(ctx, SysResetReq{ResetType: resetType}, resetInd) 8 | return resetInd.Version, err 9 | } 10 | 11 | type ResetType uint8 12 | 13 | const ( 14 | Hard ResetType = 0 15 | Soft ResetType = 1 16 | ) 17 | 18 | type SysResetReq struct { 19 | ResetType ResetType 20 | } 21 | 22 | const SysResetReqID uint8 = 0x00 23 | 24 | type ResetReason uint8 25 | 26 | const ( 27 | PowerUp ResetReason = 0 28 | External ResetReason = 1 29 | Watchdog ResetReason = 2 30 | ) 31 | 32 | type SysResetInd struct { 33 | Reason ResetReason 34 | Version Version 35 | } 36 | 37 | type Version struct { 38 | TransportRevision uint8 39 | ProductID uint8 40 | MajorRelease uint8 41 | MinorRelease uint8 42 | HardwareRevision uint8 43 | } 44 | 45 | func (v Version) IsV3() bool { 46 | return v.ProductID > 0 47 | } 48 | 49 | const SysResetIndID uint8 = 0x80 50 | -------------------------------------------------------------------------------- /adapter_reset_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/shimmeringbee/bytecodec" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func Test_resetMessages(t *testing.T) { 14 | t.Run("verify SysResetReq marshals", func(t *testing.T) { 15 | req := SysResetReq{ResetType: Soft} 16 | 17 | data, err := bytecodec.Marshal(req) 18 | 19 | assert.NoError(t, err) 20 | assert.Equal(t, []byte{0x01}, data) 21 | }) 22 | 23 | t.Run("verify SysResetInd marshals", func(t *testing.T) { 24 | req := SysResetInd{ 25 | Reason: External, 26 | Version: Version{ 27 | TransportRevision: 2, 28 | ProductID: 1, 29 | MajorRelease: 2, 30 | MinorRelease: 4, 31 | HardwareRevision: 1, 32 | }, 33 | } 34 | 35 | data, err := bytecodec.Marshal(req) 36 | 37 | assert.NoError(t, err) 38 | assert.Equal(t, []byte{0x01, 0x02, 0x01, 0x02, 0x04, 0x01}, data) 39 | }) 40 | } 41 | 42 | type MockRequestResponder struct { 43 | mock.Mock 44 | } 45 | 46 | func (m *MockRequestResponder) RequestResponse(ctx context.Context, req interface{}, resp interface{}) error { 47 | args := m.Called(ctx, req, resp) 48 | return args.Error(0) 49 | } 50 | 51 | func Test_resetAdapter(t *testing.T) { 52 | t.Run("verifies that a request response is made to unpi", func(t *testing.T) { 53 | mrr := new(MockRequestResponder) 54 | defer mrr.AssertExpectations(t) 55 | 56 | expectedVersion := Version{ 57 | TransportRevision: 1, 58 | ProductID: 2, 59 | MajorRelease: 3, 60 | MinorRelease: 4, 61 | HardwareRevision: 5, 62 | } 63 | 64 | mrr.On("RequestResponse", mock.Anything, SysResetReq{ResetType: Soft}, &SysResetInd{}).Return(nil).Run(func(args mock.Arguments) { 65 | sysResetInd := args.Get(2).(*SysResetInd) 66 | sysResetInd.Version = expectedVersion 67 | }) 68 | 69 | z := ZStack{requestResponder: mrr} 70 | 71 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 72 | defer cancel() 73 | 74 | actualVersion, err := z.resetAdapter(ctx, Soft) 75 | assert.NoError(t, err) 76 | assert.Equal(t, expectedVersion, actualVersion) 77 | }) 78 | 79 | t.Run("verifies that a request response with errors is raised", func(t *testing.T) { 80 | mrr := new(MockRequestResponder) 81 | defer mrr.AssertExpectations(t) 82 | 83 | mrr.On("RequestResponse", mock.Anything, SysResetReq{ResetType: Soft}, &SysResetInd{}).Return(errors.New("context expired")) 84 | 85 | z := ZStack{requestResponder: mrr} 86 | 87 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 88 | defer cancel() 89 | 90 | _, err := z.resetAdapter(ctx, Soft) 91 | assert.Error(t, err) 92 | }) 93 | } 94 | 95 | func Test_Version(t *testing.T) { 96 | t.Run("Version.IsV3 returns true if Version.ProductId > 0", func(t *testing.T) { 97 | assert.False(t, Version{ProductID: 0}.IsV3()) 98 | assert.True(t, Version{ProductID: 1}.IsV3()) 99 | assert.True(t, Version{ProductID: 2}.IsV3()) 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /device_state.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import "github.com/shimmeringbee/zigbee" 4 | 5 | type ZdoEndDeviceAnnceIndCapabilities struct { 6 | AddressAllocated bool `bcfieldwidth:"1"` 7 | SecurityCapability bool `bcfieldwidth:"1"` 8 | Reserved uint8 `bcfieldwidth:"2"` 9 | ReceiveOnIdle bool `bcfieldwidth:"1"` 10 | PowerSource bool `bcfieldwidth:"1"` 11 | Router bool `bcfieldwidth:"1"` 12 | AltPANController bool `bcfieldwidth:"1"` 13 | } 14 | 15 | type ZdoEndDeviceAnnceInd struct { 16 | SourceAddress zigbee.NetworkAddress 17 | NetworkAddress zigbee.NetworkAddress 18 | IEEEAddress zigbee.IEEEAddress 19 | Capabilities ZdoEndDeviceAnnceIndCapabilities 20 | } 21 | 22 | const ZdoEndDeviceAnnceIndID uint8 = 0xc1 23 | 24 | type ZdoLeaveInd struct { 25 | SourceAddress zigbee.NetworkAddress 26 | IEEEAddress zigbee.IEEEAddress 27 | Request bool 28 | Remove bool 29 | Rejoin bool 30 | } 31 | 32 | const ZdoLeaveIndID uint8 = 0xc9 33 | 34 | type ZdoTcDevInd struct { 35 | NetworkAddress zigbee.NetworkAddress 36 | IEEEAddress zigbee.IEEEAddress 37 | ParentAddress zigbee.NetworkAddress 38 | } 39 | 40 | const ZdoTcDevIndID uint8 = 0xca 41 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func (z *ZStack) sendEvent(event interface{}) { 8 | z.events <- event 9 | } 10 | 11 | func (z *ZStack) ReadEvent(ctx context.Context) (interface{}, error) { 12 | select { 13 | case event := <-z.events: 14 | return event, nil 15 | case <-ctx.Done(): 16 | return nil, context.DeadlineExceeded 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /events_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/persistence/impl/memory" 6 | unpiTest "github.com/shimmeringbee/unpi/testing" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func Test_ReadEvent(t *testing.T) { 13 | t.Run("errors if context times out", func(t *testing.T) { 14 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 15 | defer cancel() 16 | 17 | unpiMock := unpiTest.NewMockAdapter() 18 | zstack := New(unpiMock, memory.New()) 19 | defer unpiMock.Stop() 20 | defer unpiMock.AssertCalls(t) 21 | 22 | _, err := zstack.ReadEvent(ctx) 23 | assert.Error(t, err) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shimmeringbee/zstack 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.2 6 | 7 | require ( 8 | github.com/shimmeringbee/bytecodec v0.0.0-20240614104652-9d31c74dcd13 9 | github.com/shimmeringbee/logwrap v0.1.3 10 | github.com/shimmeringbee/persistence v0.0.0-20240615141034-6414db99d48e 11 | github.com/shimmeringbee/retry v0.0.0-20240614104711-064c2726a8b4 12 | github.com/shimmeringbee/unpi v0.0.0-20240714070717-115f7e5e7d4a 13 | github.com/shimmeringbee/zigbee v0.0.0-20240614104723-f4c0c0231568 14 | github.com/stretchr/testify v1.9.0 15 | golang.org/x/sync v0.7.0 16 | ) 17 | 18 | require ( 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | github.com/shimmeringbee/zcl v0.0.0-20240614104719-4eee02c0ffd1 // indirect 22 | github.com/stretchr/objx v0.5.2 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/shimmeringbee/bytecodec v0.0.0-20200216120857-49d677293817/go.mod h1:J/gvzi9IgGBHP1cBn++bqJ4tchSbgS10N2lmGMlqD3M= 8 | github.com/shimmeringbee/bytecodec v0.0.0-20210111165458-877359ca1003/go.mod h1:iqI5PkiqY+Xq6Hu22TNhepAY00iJCfk9jiXKBUrMSQQ= 9 | github.com/shimmeringbee/bytecodec v0.0.0-20210228205504-1e9e0677347b h1:8q7X6JQwYKjnl+Absfv9m+LbDSBBllqTDDKzmtZ1ybY= 10 | github.com/shimmeringbee/bytecodec v0.0.0-20210228205504-1e9e0677347b/go.mod h1:WYnxfxTJ45UQ+xeAuuTSIalcEepgP8Rb7T/OhCaDdgo= 11 | github.com/shimmeringbee/bytecodec v0.0.0-20240614104652-9d31c74dcd13 h1:GiNQq9XzoQerCE/eI8/NEPJ7W+iy0FRSn6whgnPlb3w= 12 | github.com/shimmeringbee/bytecodec v0.0.0-20240614104652-9d31c74dcd13/go.mod h1:WYnxfxTJ45UQ+xeAuuTSIalcEepgP8Rb7T/OhCaDdgo= 13 | github.com/shimmeringbee/logwrap v0.1.3 h1:1PqPGdgbeQxACQqc6RUWERn7EnpA1jbiHzXVYFa7q2A= 14 | github.com/shimmeringbee/logwrap v0.1.3/go.mod h1:NBAcZCUl6aFOGnWTs8m67EUAmWFZXRhoRQf5nknY8W0= 15 | github.com/shimmeringbee/persistence v0.0.0-20240521204303-bf4ab8a6b71b h1:hxMT4WUvcmJVzv4EgSGq4LvEoUZFdysWIznFTJoSRIU= 16 | github.com/shimmeringbee/persistence v0.0.0-20240521204303-bf4ab8a6b71b/go.mod h1:Z5euPm65BHgTSRFgaWHByaXejU/J4oUqESV9k0VzQDU= 17 | github.com/shimmeringbee/persistence v0.0.0-20240614103725-eec3415bbc98 h1:JoPko6ryVEW42pd4elhxbACWG1Jsf0EAzuh0ZOPNiPM= 18 | github.com/shimmeringbee/persistence v0.0.0-20240614103725-eec3415bbc98/go.mod h1:Z5euPm65BHgTSRFgaWHByaXejU/J4oUqESV9k0VzQDU= 19 | github.com/shimmeringbee/persistence v0.0.0-20240614122634-f587e84f4d9e h1:2D/91t0thwTrZrFhAPqEBwEUJceHbWXujhB1BqTVLFA= 20 | github.com/shimmeringbee/persistence v0.0.0-20240614122634-f587e84f4d9e/go.mod h1:Z5euPm65BHgTSRFgaWHByaXejU/J4oUqESV9k0VzQDU= 21 | github.com/shimmeringbee/persistence v0.0.0-20240614162125-05e0f3a3a718 h1:iX4hgz0ye0t2ZR+ny3O8U8vQGW2TSm4x7jY0YdqNiCM= 22 | github.com/shimmeringbee/persistence v0.0.0-20240614162125-05e0f3a3a718/go.mod h1:Z5euPm65BHgTSRFgaWHByaXejU/J4oUqESV9k0VzQDU= 23 | github.com/shimmeringbee/persistence v0.0.0-20240614163143-a99424e4d61c h1:c86y9DJNOsvZHZ/XAwfzdIxmvPPRPsWhqylF8Ok6xfg= 24 | github.com/shimmeringbee/persistence v0.0.0-20240614163143-a99424e4d61c/go.mod h1:Ob1eKGYM7+9P3LkB9vB9nr15d3trtS4D9KOnGoxOkp8= 25 | github.com/shimmeringbee/persistence v0.0.0-20240615120714-a567d1ac9349 h1:IQ2JhhUVsATqnGav6KOJRqLFDZ80wCYYG3yawTVlylI= 26 | github.com/shimmeringbee/persistence v0.0.0-20240615120714-a567d1ac9349/go.mod h1:Ob1eKGYM7+9P3LkB9vB9nr15d3trtS4D9KOnGoxOkp8= 27 | github.com/shimmeringbee/persistence v0.0.0-20240615141034-6414db99d48e h1:hxB9Zczd27kKbOMoiK56CaHgFKDyIYQLVCD1WV+LlgA= 28 | github.com/shimmeringbee/persistence v0.0.0-20240615141034-6414db99d48e/go.mod h1:Ob1eKGYM7+9P3LkB9vB9nr15d3trtS4D9KOnGoxOkp8= 29 | github.com/shimmeringbee/retry v0.0.0-20221006193055-2ce01bf139c2 h1:HxpPz7w7SxVf1GmcM5oTK1JK64TGpK1UflweYRSOwC4= 30 | github.com/shimmeringbee/retry v0.0.0-20221006193055-2ce01bf139c2/go.mod h1:KYvVq5b7/BSSlWng+AKB5jwNGpc0D7eg8ySWrdPAlms= 31 | github.com/shimmeringbee/retry v0.0.0-20240614104711-064c2726a8b4 h1:YU77guV/6/9nJymm4K1JH6MIx6yE/NfUnFX//yo3GfM= 32 | github.com/shimmeringbee/retry v0.0.0-20240614104711-064c2726a8b4/go.mod h1:KYvVq5b7/BSSlWng+AKB5jwNGpc0D7eg8ySWrdPAlms= 33 | github.com/shimmeringbee/unpi v0.0.0-20210111165207-f0210c6942fc/go.mod h1:iAt5R5HT+VC7B9U77uBmN5Z6+DJo4U0z6ag68NH2mMw= 34 | github.com/shimmeringbee/unpi v0.0.0-20210525151328-7ede275a1033 h1:PQGdXelNwwcQH58S90MR0xA3GnikCnzt+xpDw0P4qxM= 35 | github.com/shimmeringbee/unpi v0.0.0-20210525151328-7ede275a1033/go.mod h1:hOrncW6hd26Z18eayp99i7hNKj0aHtUx1SxXT49aEsk= 36 | github.com/shimmeringbee/unpi v0.0.0-20240614104715-5284f961bafc h1:rK5Dsb3RAoJZcNCsGbFvn8QkSKRWPTWHJFgjU0pCupg= 37 | github.com/shimmeringbee/unpi v0.0.0-20240614104715-5284f961bafc/go.mod h1:hOrncW6hd26Z18eayp99i7hNKj0aHtUx1SxXT49aEsk= 38 | github.com/shimmeringbee/unpi v0.0.0-20240714070717-115f7e5e7d4a h1:40e2ys9rJK58Zd+5QdySfbWi0NUpLsKdhqgEouluqaA= 39 | github.com/shimmeringbee/unpi v0.0.0-20240714070717-115f7e5e7d4a/go.mod h1:hOrncW6hd26Z18eayp99i7hNKj0aHtUx1SxXT49aEsk= 40 | github.com/shimmeringbee/zcl v0.0.0-20240614104719-4eee02c0ffd1 h1:19JMz+jKs8poUPlmF769Z2e+zZjmACS+aLB2BHFTKHE= 41 | github.com/shimmeringbee/zcl v0.0.0-20240614104719-4eee02c0ffd1/go.mod h1:DeGINQ0C9S61qBON9Zm2RArEBX4ap1LyHClfUgSUTEM= 42 | github.com/shimmeringbee/zigbee v0.0.0-20240614090423-d67fd427d102 h1:SNuznHuBvY1iEbkOEP0jbmIvn2p0GQGlCNQAUyDmcRQ= 43 | github.com/shimmeringbee/zigbee v0.0.0-20240614090423-d67fd427d102/go.mod h1:k5LLUXiOWq3hlNvMecCZRqamocgH9Zp9ocadrAfyCpw= 44 | github.com/shimmeringbee/zigbee v0.0.0-20240614103911-3a30074e1528 h1:D5jQVQ/kMjiVp4bYYmuWdKvW81+1tv2arSTgiXKkWmM= 45 | github.com/shimmeringbee/zigbee v0.0.0-20240614103911-3a30074e1528/go.mod h1:BDCm9qtlJANPiLY+YRQac/0awPxeUd3FUxUFPh+1w/s= 46 | github.com/shimmeringbee/zigbee v0.0.0-20240614104723-f4c0c0231568 h1:DnZ/kbXJZtihjqB7mz92hhUeP0+v0jYl5DJIznWdlL4= 47 | github.com/shimmeringbee/zigbee v0.0.0-20240614104723-f4c0c0231568/go.mod h1:BDCm9qtlJANPiLY+YRQac/0awPxeUd3FUxUFPh+1w/s= 48 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 53 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 54 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 55 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 56 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 57 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 58 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 59 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 61 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 62 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 63 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 64 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 65 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 66 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 70 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 71 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 72 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 73 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 74 | -------------------------------------------------------------------------------- /joining.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) PermitJoin(ctx context.Context, allRouters bool) error { 10 | if allRouters { 11 | return z.sendJoin(ctx, zigbee.BroadcastRoutersCoordinators, JoiningOn, OnAllRouters) 12 | } else { 13 | return z.sendJoin(ctx, z.NetworkProperties.NetworkAddress, JoiningOn, OnCoordinator) 14 | } 15 | } 16 | 17 | func (z *ZStack) DenyJoin(ctx context.Context) error { 18 | return z.sendJoin(ctx, zigbee.BroadcastRoutersCoordinators, JoiningOff, Off) 19 | } 20 | 21 | func (z *ZStack) sendJoin(ctx context.Context, address zigbee.NetworkAddress, timeout uint8, newState JoinState) error { 22 | if err := z.sem.Acquire(ctx, 1); err != nil { 23 | return fmt.Errorf("failed to acquire semaphore: %w", err) 24 | } 25 | defer z.sem.Release(1) 26 | 27 | response := ZDOMgmtPermitJoinRequestReply{} 28 | 29 | if err := z.requestResponder.RequestResponse(ctx, ZDOMgmtPermitJoinRequest{ 30 | Destination: address, 31 | Duration: timeout, 32 | TCSignificance: 0x00, 33 | }, &response); err != nil { 34 | return err 35 | } 36 | 37 | if response.Status != ZSuccess { 38 | return fmt.Errorf("adapter rejected permit join state change: state=%v", response.Status) 39 | } 40 | 41 | z.NetworkProperties.JoinState = newState 42 | 43 | return nil 44 | } 45 | 46 | const ( 47 | JoiningOff uint8 = 0x00 48 | JoiningOn uint8 = 0xff 49 | ) 50 | 51 | type ZDOMgmtPermitJoinRequest struct { 52 | Destination zigbee.NetworkAddress 53 | Duration uint8 54 | TCSignificance uint8 55 | } 56 | 57 | const ZDOMgmtPermitJoinRequestID = 0x36 58 | 59 | type ZDOMgmtPermitJoinRequestReply GenericZStackStatus 60 | 61 | const ZDOMgmtPermitJoinRequestReplyID uint8 = 0x36 62 | -------------------------------------------------------------------------------- /joining_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_PermitJoin(t *testing.T) { 17 | t.Run("permit join for all routers sends message to all routers permitting joining", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | zstack := New(unpiMock, memory.New()) 23 | zstack.sem = semaphore.NewWeighted(8) 24 | defer unpiMock.Stop() 25 | 26 | c := unpiMock.On(SREQ, ZDO, ZDOMgmtPermitJoinRequestID).Return(Frame{ 27 | MessageType: SRSP, 28 | Subsystem: ZDO, 29 | CommandID: ZDOMgmtPermitJoinRequestReplyID, 30 | Payload: []byte{0x00}, 31 | }) 32 | 33 | err := zstack.PermitJoin(ctx, true) 34 | assert.NoError(t, err) 35 | 36 | unpiMock.AssertCalls(t) 37 | 38 | assert.Equal(t, []byte{0xfc, 0xff, 0xff, 0x00}, c.CapturedCalls[0].Frame.Payload) 39 | assert.Equal(t, OnAllRouters, zstack.NetworkProperties.JoinState) 40 | }) 41 | 42 | t.Run("permit join for the coordinator sends message to coordinator permitting joining", func(t *testing.T) { 43 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 44 | defer cancel() 45 | 46 | unpiMock := unpiTest.NewMockAdapter() 47 | zstack := New(unpiMock, memory.New()) 48 | zstack.sem = semaphore.NewWeighted(8) 49 | defer unpiMock.Stop() 50 | zstack.NetworkProperties.NetworkAddress = zigbee.NetworkAddress(0x0102) 51 | 52 | c := unpiMock.On(SREQ, ZDO, ZDOMgmtPermitJoinRequestID).Return(Frame{ 53 | MessageType: SRSP, 54 | Subsystem: ZDO, 55 | CommandID: ZDOMgmtPermitJoinRequestReplyID, 56 | Payload: []byte{0x00}, 57 | }) 58 | 59 | err := zstack.PermitJoin(ctx, false) 60 | assert.NoError(t, err) 61 | 62 | unpiMock.AssertCalls(t) 63 | 64 | assert.Equal(t, []byte{0x02, 0x01, 0xff, 0x00}, c.CapturedCalls[0].Frame.Payload) 65 | assert.Equal(t, OnCoordinator, zstack.NetworkProperties.JoinState) 66 | }) 67 | 68 | t.Run("permit join rejection by adapter errors", func(t *testing.T) { 69 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 70 | defer cancel() 71 | 72 | unpiMock := unpiTest.NewMockAdapter() 73 | zstack := New(unpiMock, memory.New()) 74 | zstack.sem = semaphore.NewWeighted(8) 75 | defer unpiMock.Stop() 76 | 77 | unpiMock.On(SREQ, ZDO, ZDOMgmtPermitJoinRequestID).Return(Frame{ 78 | MessageType: SRSP, 79 | Subsystem: ZDO, 80 | CommandID: ZDOMgmtPermitJoinRequestReplyID, 81 | Payload: []byte{0x01}, 82 | }) 83 | 84 | err := zstack.PermitJoin(ctx, true) 85 | assert.Error(t, err) 86 | 87 | unpiMock.AssertCalls(t) 88 | }) 89 | } 90 | 91 | func Test_DenyJoin(t *testing.T) { 92 | t.Run("denying join sends message to all routers disabling joining", func(t *testing.T) { 93 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 94 | defer cancel() 95 | 96 | unpiMock := unpiTest.NewMockAdapter() 97 | zstack := New(unpiMock, memory.New()) 98 | zstack.sem = semaphore.NewWeighted(8) 99 | defer unpiMock.Stop() 100 | 101 | c := unpiMock.On(SREQ, ZDO, ZDOMgmtPermitJoinRequestID).Return(Frame{ 102 | MessageType: SRSP, 103 | Subsystem: ZDO, 104 | CommandID: ZDOMgmtPermitJoinRequestReplyID, 105 | Payload: []byte{0x00}, 106 | }) 107 | 108 | zstack.NetworkProperties.JoinState = OnCoordinator 109 | err := zstack.DenyJoin(ctx) 110 | assert.NoError(t, err) 111 | 112 | unpiMock.AssertCalls(t) 113 | 114 | assert.Equal(t, []byte{0xfc, 0xff, 0x00, 0x00}, c.CapturedCalls[0].Frame.Payload) 115 | assert.Equal(t, Off, zstack.NetworkProperties.JoinState) 116 | }) 117 | 118 | t.Run("denying join rejection by adapter errors", func(t *testing.T) { 119 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 120 | defer cancel() 121 | 122 | unpiMock := unpiTest.NewMockAdapter() 123 | zstack := New(unpiMock, memory.New()) 124 | zstack.sem = semaphore.NewWeighted(8) 125 | defer unpiMock.Stop() 126 | 127 | unpiMock.On(SREQ, ZDO, ZDOMgmtPermitJoinRequestID).Return(Frame{ 128 | MessageType: SRSP, 129 | Subsystem: ZDO, 130 | CommandID: ZDOMgmtPermitJoinRequestReplyID, 131 | Payload: []byte{0x01}, 132 | }) 133 | 134 | err := zstack.DenyJoin(ctx) 135 | assert.Error(t, err) 136 | 137 | unpiMock.AssertCalls(t) 138 | }) 139 | } 140 | 141 | func Test_ZDOMgmtPermitJoin(t *testing.T) { 142 | t.Run("ZDOMgmtPermitJoinRequest", func(t *testing.T) { 143 | s := ZDOMgmtPermitJoinRequest{ 144 | Destination: 0x0102, 145 | Duration: 0x03, 146 | TCSignificance: 0x04, 147 | } 148 | 149 | actualBytes, err := bytecodec.Marshal(s) 150 | 151 | expectedBytes := []byte{0x02, 0x01, 0x03, 0x04} 152 | 153 | assert.NoError(t, err) 154 | assert.Equal(t, expectedBytes, actualBytes) 155 | }) 156 | 157 | t.Run("ZDOMgmtPermitJoinRequestReply", func(t *testing.T) { 158 | s := ZDOMgmtPermitJoinRequestReply{ 159 | Status: 0x01, 160 | } 161 | 162 | actualBytes, err := bytecodec.Marshal(s) 163 | 164 | expectedBytes := []byte{0x01} 165 | 166 | assert.NoError(t, err) 167 | assert.Equal(t, expectedBytes, actualBytes) 168 | }) 169 | } 170 | -------------------------------------------------------------------------------- /messages.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "errors" 5 | . "github.com/shimmeringbee/unpi" 6 | . "github.com/shimmeringbee/unpi/library" 7 | ) 8 | 9 | func registerMessages(l *Library) { 10 | l.Add(AREQ, SYS, SysResetReqID, SysResetReq{}) 11 | l.Add(AREQ, SYS, SysResetIndID, SysResetInd{}) 12 | 13 | l.Add(SREQ, SYS, SysOSALNVReadID, SysOSALNVRead{}) 14 | l.Add(SRSP, SYS, SysOSALNVReadReplyID, SysOSALNVReadReply{}) 15 | 16 | l.Add(SREQ, SYS, SysOSALNVWriteID, SysOSALNVWrite{}) 17 | l.Add(SRSP, SYS, SysOSALNVWriteReplyID, SysOSALNVWriteReply{}) 18 | 19 | l.Add(AREQ, ZDO, ZDOStateChangeIndID, ZDOStateChangeInd{}) 20 | 21 | l.Add(AREQ, ZDO, ZdoEndDeviceAnnceIndID, ZdoEndDeviceAnnceInd{}) 22 | l.Add(AREQ, ZDO, ZdoLeaveIndID, ZdoLeaveInd{}) 23 | l.Add(AREQ, ZDO, ZdoTcDevIndID, ZdoTcDevInd{}) 24 | 25 | l.Add(SREQ, ZDO, ZdoMGMTLQIReqID, ZdoMGMTLQIReq{}) 26 | l.Add(SRSP, ZDO, ZdoMGMTLQIReqReplyID, ZdoMGMTLQIReqReply{}) 27 | l.Add(AREQ, ZDO, ZdoMGMTLQIRspID, ZdoMGMTLQIRsp{}) 28 | 29 | l.Add(SREQ, AF, AFRegisterID, AFRegister{}) 30 | l.Add(SRSP, AF, AFRegisterReplyID, AFRegisterReply{}) 31 | 32 | l.Add(SREQ, ZDO, ZdoActiveEpReqID, ZdoActiveEpReq{}) 33 | l.Add(SRSP, ZDO, ZdoActiveEpReqReplyID, ZdoActiveEpReqReply{}) 34 | l.Add(AREQ, ZDO, ZdoActiveEpRspID, ZdoActiveEpRsp{}) 35 | 36 | l.Add(SREQ, ZDO, ZdoSimpleDescReqID, ZdoSimpleDescReq{}) 37 | l.Add(SRSP, ZDO, ZdoSimpleDescReqReplyID, ZdoSimpleDescReqReply{}) 38 | l.Add(AREQ, ZDO, ZdoSimpleDescRspID, ZdoSimpleDescRsp{}) 39 | 40 | l.Add(SREQ, ZDO, ZdoNodeDescReqID, ZdoNodeDescReq{}) 41 | l.Add(SRSP, ZDO, ZdoNodeDescReqReplyID, ZdoNodeDescReqReply{}) 42 | l.Add(AREQ, ZDO, ZdoNodeDescRspID, ZdoNodeDescRsp{}) 43 | 44 | l.Add(SREQ, ZDO, ZdoBindReqID, ZdoBindReq{}) 45 | l.Add(SRSP, ZDO, ZdoBindReqReplyID, ZdoBindReqReply{}) 46 | l.Add(AREQ, ZDO, ZdoBindRspID, ZdoBindRsp{}) 47 | 48 | l.Add(SREQ, ZDO, ZdoUnbindReqID, ZdoUnbindReq{}) 49 | l.Add(SRSP, ZDO, ZdoUnbindReqReplyID, ZdoUnbindReqReply{}) 50 | l.Add(AREQ, ZDO, ZdoUnbindRspID, ZdoUnbindRsp{}) 51 | 52 | l.Add(AREQ, AF, AfIncomingMsgID, AfIncomingMsg{}) 53 | 54 | l.Add(SREQ, ZDO, ZdoIEEEAddrReqID, ZdoIEEEAddrReq{}) 55 | l.Add(SRSP, ZDO, ZdoIEEEAddrReqReplyID, ZdoIEEEAddrReqReply{}) 56 | l.Add(AREQ, ZDO, ZdoIEEEAddrRspID, ZdoIEEEAddrRsp{}) 57 | 58 | l.Add(SREQ, AF, AfDataRequestID, AfDataRequest{}) 59 | l.Add(SRSP, AF, AfDataRequestReplyID, AfDataRequestReply{}) 60 | l.Add(AREQ, AF, AfDataConfirmID, AfDataConfirm{}) 61 | 62 | l.Add(SREQ, ZDO, ZdoNWKAddrReqID, ZdoNWKAddrReq{}) 63 | l.Add(SRSP, ZDO, ZdoNWKAddrReqReplyID, ZdoNWKAddrReqReply{}) 64 | l.Add(AREQ, ZDO, ZdoNWKAddrRspID, ZdoNWKAddrRsp{}) 65 | 66 | l.Add(SREQ, APP_CNF, APPCNFBDBStartCommissioningRequestID, APPCNFBDBStartCommissioningRequest{}) 67 | l.Add(SRSP, APP_CNF, APPCNFBDBStartCommissioningRequestReplyID, APPCNFBDBStartCommissioningRequestReply{}) 68 | 69 | l.Add(SREQ, APP_CNF, APPCNFBDBSetChannelRequestID, APPCNFBDBSetChannelRequest{}) 70 | l.Add(SRSP, APP_CNF, APPCNFBDBSetChannelRequestReplyID, APPCNFBDBSetChannelRequestReply{}) 71 | 72 | l.Add(SREQ, ZDO, ZDOStartUpFromAppRequestId, ZDOStartUpFromAppRequest{}) 73 | l.Add(SRSP, ZDO, ZDOStartUpFromAppRequestReplyID, ZDOStartUpFromAppRequestReply{}) 74 | 75 | l.Add(SREQ, UTIL, UtilGetDeviceInfoRequestID, UtilGetDeviceInfoRequest{}) 76 | l.Add(SRSP, UTIL, UtilGetDeviceInfoRequestReplyID, UtilGetDeviceInfoRequestReply{}) 77 | 78 | l.Add(SREQ, ZDO, ZDOMgmtPermitJoinRequestID, ZDOMgmtPermitJoinRequest{}) 79 | l.Add(SRSP, ZDO, ZDOMgmtPermitJoinRequestReplyID, ZDOMgmtPermitJoinRequestReply{}) 80 | 81 | l.Add(SREQ, ZDO, ZdoMgmtLeaveReqID, ZdoMgmtLeaveReq{}) 82 | l.Add(SRSP, ZDO, ZdoMgmtLeaveReqReplyID, ZdoMgmtLeaveReqReply{}) 83 | l.Add(AREQ, ZDO, ZdoMgmtLeaveRspID, ZdoMgmtLeaveRsp{}) 84 | } 85 | 86 | type ZStackStatus uint8 87 | 88 | type Successor interface { 89 | WasSuccessful() bool 90 | } 91 | 92 | type GenericZStackStatus struct { 93 | Status ZStackStatus 94 | } 95 | 96 | func (s GenericZStackStatus) WasSuccessful() bool { 97 | return s.Status == ZSuccess 98 | } 99 | 100 | var ErrorZFailure = errors.New("ZStack has returned a failure") 101 | 102 | const ( 103 | ZSuccess ZStackStatus = 0x00 104 | ZFailure ZStackStatus = 0x01 105 | ) 106 | -------------------------------------------------------------------------------- /network_manager.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/logwrap" 6 | "github.com/shimmeringbee/zigbee" 7 | "reflect" 8 | "time" 9 | ) 10 | 11 | const defaultPollingInterval = 30 12 | 13 | func (z *ZStack) startNetworkManager() { 14 | go z.networkManager() 15 | } 16 | 17 | func (z *ZStack) stopNetworkManager() { 18 | z.networkManagerStop <- true 19 | } 20 | 21 | func (z *ZStack) networkManager() { 22 | z.nodeTable.addOrUpdate(z.NetworkProperties.IEEEAddress, z.NetworkProperties.NetworkAddress, logicalType(zigbee.Coordinator)) 23 | 24 | immediateStart := make(chan bool, 1) 25 | defer close(immediateStart) 26 | immediateStart <- true 27 | 28 | _, cancel := z.subscriber.Subscribe(&ZdoMGMTLQIRsp{}, z.receiveLQIUpdate) 29 | defer cancel() 30 | 31 | _, cancel = z.subscriber.Subscribe(&ZdoEndDeviceAnnceInd{}, z.receiveEndDeviceAnnouncement) 32 | defer cancel() 33 | 34 | _, cancel = z.subscriber.Subscribe(&ZdoLeaveInd{}, z.receiveLeaveAnnouncement) 35 | defer cancel() 36 | 37 | _, cancel = z.subscriber.Subscribe(&ZdoIEEEAddrRsp{}, z.receiveIEEEAddrRsp) 38 | defer cancel() 39 | 40 | _, cancel = z.subscriber.Subscribe(&ZdoNWKAddrRsp{}, z.receiveNWKAddrRsp) 41 | defer cancel() 42 | 43 | z.nodeTable.registerCallback(z.nodeTableUpdate) 44 | 45 | for { 46 | select { 47 | case <-immediateStart: 48 | z.pollRoutersForNetworkStatus() 49 | case <-time.After(defaultPollingInterval * time.Second): 50 | z.pollRoutersForNetworkStatus() 51 | case <-z.networkManagerStop: 52 | return 53 | case ue := <-z.networkManagerIncoming: 54 | switch e := ue.(type) { 55 | case ZdoMGMTLQIRsp: 56 | z.processLQITable(e) 57 | case ZdoEndDeviceAnnceInd: 58 | z.newNode(e) 59 | case ZdoLeaveInd: 60 | z.removeNode(e.IEEEAddress) 61 | case ZdoIEEEAddrRsp: 62 | if e.WasSuccessful() { 63 | z.nodeTable.addOrUpdate(e.IEEEAddress, e.NetworkAddress, updateDiscovered()) 64 | } 65 | case ZdoNWKAddrRsp: 66 | if e.WasSuccessful() { 67 | z.nodeTable.addOrUpdate(e.IEEEAddress, e.NetworkAddress, updateDiscovered()) 68 | } 69 | default: 70 | z.logger.LogWarn(context.Background(), "Received unknown message type from unpi.", logwrap.Datum("Type", reflect.TypeOf(ue))) 71 | } 72 | } 73 | } 74 | } 75 | 76 | func (z *ZStack) newNode(e ZdoEndDeviceAnnceInd) { 77 | deviceLogicalType := zigbee.EndDevice 78 | 79 | if e.Capabilities.Router { 80 | deviceLogicalType = zigbee.Router 81 | } 82 | 83 | z.nodeTable.addOrUpdate(e.IEEEAddress, e.NetworkAddress, logicalType(deviceLogicalType), updateDiscovered(), updateReceived()) 84 | node, _ := z.nodeTable.getByIEEE(e.IEEEAddress) 85 | 86 | z.sendEvent(zigbee.NodeJoinEvent{ 87 | Node: node, 88 | }) 89 | 90 | if deviceLogicalType == zigbee.Router { 91 | node, _ := z.nodeTable.getByIEEE(e.IEEEAddress) 92 | go z.pollNodeForNetworkStatus(node) 93 | } 94 | } 95 | 96 | func (z *ZStack) removeNode(ieee zigbee.IEEEAddress) bool { 97 | node, found := z.nodeTable.getByIEEE(ieee) 98 | z.nodeTable.remove(ieee) 99 | 100 | if found { 101 | z.sendEvent(zigbee.NodeLeaveEvent{ 102 | Node: node, 103 | }) 104 | } 105 | 106 | return found 107 | } 108 | 109 | func (z *ZStack) pollRoutersForNetworkStatus() { 110 | for _, node := range z.nodeTable.nodes() { 111 | if node.LogicalType == zigbee.Coordinator || node.LogicalType == zigbee.Router { 112 | go z.pollNodeForNetworkStatus(node) 113 | } 114 | } 115 | } 116 | 117 | func (z *ZStack) pollNodeForNetworkStatus(node zigbee.Node) { 118 | z.logger.LogDebug(context.Background(), "Polling device for network status.", logwrap.Datum("IEEEAddress", node.IEEEAddress.String()), logwrap.Datum("NetworkAddress", node.NetworkAddress)) 119 | z.requestLQITable(node, 0) 120 | } 121 | 122 | func (z *ZStack) requestLQITable(node zigbee.Node, startIndex uint8) { 123 | ctx, cancel := context.WithTimeout(context.Background(), DefaultZStackTimeout) 124 | defer cancel() 125 | 126 | if err := z.sem.Acquire(ctx, 1); err != nil { 127 | z.logger.LogError(ctx, "Failed to request LQI table, failed to acquire semaphore ", logwrap.Datum("IEEEAddress", node.IEEEAddress.String()), logwrap.Datum("NetworkAddress", node.NetworkAddress), logwrap.Err(err)) 128 | return 129 | } 130 | defer z.sem.Release(1) 131 | 132 | resp := ZdoMGMTLQIReqReply{} 133 | z.logger.LogDebug(ctx, "Requesting LQI table from device.", logwrap.Datum("IEEEAddress", node.IEEEAddress.String()), logwrap.Datum("NetworkAddress", node.NetworkAddress), logwrap.Datum("StartIndex", startIndex)) 134 | if err := z.requestResponder.RequestResponse(ctx, ZdoMGMTLQIReq{DestinationAddress: node.NetworkAddress, StartIndex: startIndex}, &resp); err != nil { 135 | z.logger.LogError(ctx, "Failed to request LQI table.", logwrap.Datum("IEEEAddress", node.IEEEAddress.String()), logwrap.Datum("NetworkAddress", node.NetworkAddress), logwrap.Err(err)) 136 | } else if resp.Status != ZSuccess { 137 | z.logger.LogError(ctx, "Failed to request LQI table, adapter returned error code.", logwrap.Datum("IEEEAddress", node.IEEEAddress.String()), logwrap.Datum("NetworkAddress", node.NetworkAddress), logwrap.Datum("Status", resp.Status)) 138 | } 139 | } 140 | 141 | func (z *ZStack) processLQITable(lqiResp ZdoMGMTLQIRsp) { 142 | if lqiResp.Status != ZSuccess { 143 | z.logger.LogError(context.Background(), "LQI table response received, but as not success.", logwrap.Datum("NetworkAddress", lqiResp.SourceAddress), logwrap.Datum("Status", lqiResp.Status)) 144 | return 145 | } 146 | 147 | z.logger.LogDebug(context.Background(), "LQI table response received.", logwrap.Datum("NetworkAddress", lqiResp.SourceAddress), logwrap.Datum("Status", lqiResp.Status), logwrap.Datum("StartIndex", lqiResp.StartIndex), logwrap.Datum("IncludedCount", len(lqiResp.Neighbors)), logwrap.Datum("NeighbourCount", lqiResp.NeighbourTableEntries)) 148 | 149 | for _, neighbour := range lqiResp.Neighbors { 150 | if neighbour.ExtendedPANID != z.NetworkProperties.ExtendedPANID || 151 | neighbour.IEEEAddress == zigbee.EmptyIEEEAddress { 152 | continue 153 | } 154 | 155 | z.nodeTable.addOrUpdate(neighbour.IEEEAddress, neighbour.NetworkAddress, logicalType(neighbour.Status.DeviceType), updateDiscovered()) 156 | 157 | if neighbour.Status.Relationship == zigbee.RelationshipChild { 158 | z.nodeTable.update(neighbour.IEEEAddress, lqi(neighbour.LQI), depth(neighbour.Depth)) 159 | } 160 | } 161 | 162 | nextIndex := uint8(int(lqiResp.StartIndex) + len(lqiResp.Neighbors)) 163 | 164 | if nextIndex < lqiResp.NeighbourTableEntries { 165 | node, found := z.nodeTable.getByNetwork(lqiResp.SourceAddress) 166 | 167 | if found { 168 | z.logger.LogDebug(context.Background(), "LQI table response requires pagination.", logwrap.Datum("NetworkAddress", lqiResp.SourceAddress), logwrap.Datum("Status", lqiResp.Status), logwrap.Datum("StartIndex", lqiResp.StartIndex), logwrap.Datum("IncludedCount", len(lqiResp.Neighbors)), logwrap.Datum("NeighbourCount", lqiResp.NeighbourTableEntries)) 169 | z.requestLQITable(node, nextIndex) 170 | } 171 | } 172 | } 173 | 174 | func (z *ZStack) receiveLQIUpdate(v interface{}) { 175 | msg := v.(*ZdoMGMTLQIRsp) 176 | z.networkManagerIncoming <- *msg 177 | } 178 | 179 | func (z *ZStack) receiveEndDeviceAnnouncement(v interface{}) { 180 | msg := v.(*ZdoEndDeviceAnnceInd) 181 | z.networkManagerIncoming <- *msg 182 | } 183 | 184 | func (z *ZStack) receiveLeaveAnnouncement(v interface{}) { 185 | msg := v.(*ZdoLeaveInd) 186 | z.networkManagerIncoming <- *msg 187 | } 188 | 189 | func (z *ZStack) receiveIEEEAddrRsp(v interface{}) { 190 | msg := v.(*ZdoIEEEAddrRsp) 191 | z.networkManagerIncoming <- *msg 192 | } 193 | 194 | func (z *ZStack) receiveNWKAddrRsp(v interface{}) { 195 | msg := v.(*ZdoNWKAddrRsp) 196 | z.networkManagerIncoming <- *msg 197 | } 198 | 199 | func (z *ZStack) nodeTableUpdate(node zigbee.Node) { 200 | z.sendEvent(zigbee.NodeUpdateEvent{ 201 | Node: node, 202 | }) 203 | } 204 | 205 | type ZdoMGMTLQIReq struct { 206 | DestinationAddress zigbee.NetworkAddress 207 | StartIndex uint8 208 | } 209 | 210 | const ZdoMGMTLQIReqID uint8 = 0x31 211 | 212 | type ZdoMGMTLQIReqReply GenericZStackStatus 213 | 214 | const ZdoMGMTLQIReqReplyID uint8 = 0x31 215 | 216 | type ZdoMGMTLQINeighbourStatus struct { 217 | Reserved uint8 `bcfieldwidth:"1"` 218 | Relationship zigbee.Relationship `bcfieldwidth:"3"` 219 | RxOnWhenIdle uint8 `bcfieldwidth:"2"` 220 | DeviceType zigbee.LogicalType `bcfieldwidth:"2"` 221 | } 222 | 223 | type ZdoMGMTLQINeighbour struct { 224 | ExtendedPANID zigbee.ExtendedPANID 225 | IEEEAddress zigbee.IEEEAddress 226 | NetworkAddress zigbee.NetworkAddress 227 | Status ZdoMGMTLQINeighbourStatus 228 | PermitJoining bool 229 | Depth uint8 230 | LQI uint8 231 | } 232 | 233 | type ZdoMGMTLQIRsp struct { 234 | SourceAddress zigbee.NetworkAddress 235 | Status ZStackStatus 236 | NeighbourTableEntries uint8 237 | StartIndex uint8 238 | Neighbors []ZdoMGMTLQINeighbour `bcsliceprefix:"8"` 239 | } 240 | 241 | const ZdoMGMTLQIRspID uint8 = 0xb1 242 | -------------------------------------------------------------------------------- /network_manager_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_NetworkManager(t *testing.T) { 17 | t.Run("issues a lqi poll request only for coordinators or routers", func(t *testing.T) { 18 | unpiMock := unpiTest.NewMockAdapter() 19 | zstack := New(unpiMock, memory.New()) 20 | zstack.sem = semaphore.NewWeighted(8) 21 | defer unpiMock.Stop() 22 | defer zstack.Stop() 23 | 24 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 25 | MessageType: SRSP, 26 | Subsystem: ZDO, 27 | CommandID: ZdoMGMTLQIReqReplyID, 28 | Payload: []byte{0x00}, 29 | }).Times(2) 30 | 31 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(1), zigbee.NetworkAddress(1), logicalType(zigbee.Router)) 32 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(2), zigbee.NetworkAddress(2), logicalType(zigbee.Unknown)) 33 | 34 | zstack.startNetworkManager() 35 | defer zstack.stopNetworkManager() 36 | 37 | time.Sleep(10 * time.Millisecond) 38 | 39 | unpiMock.AssertCalls(t) 40 | }) 41 | 42 | t.Run("the coordinator is added to the node list as a coordinator", func(t *testing.T) { 43 | unpiMock := unpiTest.NewMockAdapter() 44 | zstack := New(unpiMock, memory.New()) 45 | zstack.sem = semaphore.NewWeighted(8) 46 | defer unpiMock.Stop() 47 | defer zstack.Stop() 48 | 49 | expectedIEEE := zigbee.IEEEAddress(0x0002) 50 | expectedAddress := zigbee.NetworkAddress(0x0001) 51 | 52 | zstack.NetworkProperties.NetworkAddress = expectedAddress 53 | zstack.NetworkProperties.IEEEAddress = expectedIEEE 54 | 55 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 56 | MessageType: SRSP, 57 | Subsystem: ZDO, 58 | CommandID: ZdoMGMTLQIReqReplyID, 59 | Payload: []byte{0x00}, 60 | }) 61 | 62 | zstack.startNetworkManager() 63 | defer zstack.stopNetworkManager() 64 | 65 | time.Sleep(10 * time.Millisecond) 66 | 67 | node, found := zstack.nodeTable.getByIEEE(expectedIEEE) 68 | 69 | assert.True(t, found) 70 | assert.Equal(t, expectedAddress, node.NetworkAddress) 71 | assert.Equal(t, zigbee.Coordinator, node.LogicalType) 72 | 73 | unpiMock.AssertCalls(t) 74 | }) 75 | 76 | t.Run("a node is added to the node table when an ZdoIEEEAddrRsp messages are received", func(t *testing.T) { 77 | unpiMock := unpiTest.NewMockAdapter() 78 | zstack := New(unpiMock, memory.New()) 79 | zstack.sem = semaphore.NewWeighted(8) 80 | defer unpiMock.Stop() 81 | defer unpiMock.AssertCalls(t) 82 | 83 | zstack.startNetworkManager() 84 | defer zstack.stopNetworkManager() 85 | 86 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 87 | MessageType: SRSP, 88 | Subsystem: ZDO, 89 | CommandID: ZdoMGMTLQIReqReplyID, 90 | Payload: []byte{0x00}, 91 | }) 92 | 93 | time.Sleep(10 * time.Millisecond) 94 | 95 | unpiMock.InjectOutgoing(Frame{ 96 | MessageType: AREQ, 97 | Subsystem: ZDO, 98 | CommandID: ZdoIEEEAddrRspID, 99 | Payload: []byte{0x00, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x40, 0x00, 0x00}, 100 | }) 101 | 102 | time.Sleep(10 * time.Millisecond) 103 | 104 | node, found := zstack.nodeTable.getByIEEE(0x1122334455667788) 105 | 106 | assert.True(t, found) 107 | assert.Equal(t, zigbee.IEEEAddress(0x1122334455667788), node.IEEEAddress) 108 | assert.Equal(t, zigbee.NetworkAddress(0x4000), node.NetworkAddress) 109 | }) 110 | 111 | t.Run("a node is added to the node table when an ZdoNWKAddrRsp messages are received", func(t *testing.T) { 112 | unpiMock := unpiTest.NewMockAdapter() 113 | zstack := New(unpiMock, memory.New()) 114 | zstack.sem = semaphore.NewWeighted(8) 115 | defer unpiMock.Stop() 116 | defer unpiMock.AssertCalls(t) 117 | 118 | zstack.startNetworkManager() 119 | defer zstack.stopNetworkManager() 120 | 121 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 122 | MessageType: SRSP, 123 | Subsystem: ZDO, 124 | CommandID: ZdoMGMTLQIReqReplyID, 125 | Payload: []byte{0x00}, 126 | }) 127 | 128 | time.Sleep(10 * time.Millisecond) 129 | 130 | unpiMock.InjectOutgoing(Frame{ 131 | MessageType: AREQ, 132 | Subsystem: ZDO, 133 | CommandID: ZdoNWKAddrRspID, 134 | Payload: []byte{0x00, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x40, 0x00, 0x00}, 135 | }) 136 | 137 | time.Sleep(10 * time.Millisecond) 138 | 139 | node, found := zstack.nodeTable.getByIEEE(0x1122334455667788) 140 | 141 | assert.True(t, found) 142 | assert.Equal(t, zigbee.IEEEAddress(0x1122334455667788), node.IEEEAddress) 143 | assert.Equal(t, zigbee.NetworkAddress(0x4000), node.NetworkAddress) 144 | }) 145 | 146 | t.Run("emits NodeJoinEvent event when node join announcement received", func(t *testing.T) { 147 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 148 | defer cancel() 149 | 150 | unpiMock := unpiTest.NewMockAdapter() 151 | zstack := New(unpiMock, memory.New()) 152 | zstack.sem = semaphore.NewWeighted(8) 153 | defer unpiMock.Stop() 154 | defer unpiMock.AssertCalls(t) 155 | 156 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 157 | MessageType: SRSP, 158 | Subsystem: ZDO, 159 | CommandID: ZdoMGMTLQIReqReplyID, 160 | Payload: []byte{0x00}, 161 | }).UnlimitedTimes() 162 | 163 | zstack.startNetworkManager() 164 | defer zstack.stopNetworkManager() 165 | 166 | time.Sleep(10 * time.Millisecond) 167 | 168 | announce := ZdoEndDeviceAnnceInd{ 169 | SourceAddress: zigbee.NetworkAddress(0x1000), 170 | NetworkAddress: zigbee.NetworkAddress(0x2000), 171 | IEEEAddress: zigbee.IEEEAddress(0x0102030405060708), 172 | Capabilities: ZdoEndDeviceAnnceIndCapabilities{ 173 | AltPANController: false, 174 | Router: true, 175 | PowerSource: false, 176 | ReceiveOnIdle: false, 177 | Reserved: 0, 178 | SecurityCapability: false, 179 | AddressAllocated: false, 180 | }, 181 | } 182 | 183 | data, _ := bytecodec.Marshal(announce) 184 | 185 | unpiMock.InjectOutgoing(Frame{ 186 | MessageType: AREQ, 187 | Subsystem: ZDO, 188 | CommandID: ZdoEndDeviceAnnceIndID, 189 | Payload: data, 190 | }) 191 | 192 | // Throw away the NodeUpdateEvent. 193 | zstack.ReadEvent(ctx) 194 | 195 | event, err := zstack.ReadEvent(ctx) 196 | assert.NoError(t, err) 197 | 198 | nodeJoin, ok := event.(zigbee.NodeJoinEvent) 199 | 200 | assert.True(t, ok) 201 | 202 | assert.Equal(t, announce.NetworkAddress, nodeJoin.NetworkAddress) 203 | assert.Equal(t, announce.IEEEAddress, nodeJoin.IEEEAddress) 204 | 205 | node, found := zstack.nodeTable.getByIEEE(announce.IEEEAddress) 206 | 207 | assert.True(t, found) 208 | assert.Equal(t, announce.IEEEAddress, node.IEEEAddress) 209 | assert.Equal(t, announce.NetworkAddress, node.NetworkAddress) 210 | assert.Equal(t, zigbee.Router, node.LogicalType) 211 | }) 212 | 213 | t.Run("emits NodeLeaveEvent event when node leave announcement received", func(t *testing.T) { 214 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 215 | defer cancel() 216 | 217 | unpiMock := unpiTest.NewMockAdapter() 218 | zstack := New(unpiMock, memory.New()) 219 | zstack.sem = semaphore.NewWeighted(8) 220 | defer unpiMock.Stop() 221 | defer unpiMock.AssertCalls(t) 222 | 223 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 224 | MessageType: SRSP, 225 | Subsystem: ZDO, 226 | CommandID: ZdoMGMTLQIReqReplyID, 227 | Payload: []byte{0x00}, 228 | }).UnlimitedTimes() 229 | 230 | zstack.startNetworkManager() 231 | defer zstack.stopNetworkManager() 232 | 233 | announce := ZdoLeaveInd{ 234 | SourceAddress: zigbee.NetworkAddress(0x2000), 235 | IEEEAddress: zigbee.IEEEAddress(0x0102030405060708), 236 | } 237 | 238 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(0x0102030405060708), zigbee.NetworkAddress(0x2000)) 239 | 240 | data, _ := bytecodec.Marshal(announce) 241 | 242 | time.Sleep(10 * time.Millisecond) 243 | 244 | unpiMock.InjectOutgoing(Frame{ 245 | MessageType: AREQ, 246 | Subsystem: ZDO, 247 | CommandID: ZdoLeaveIndID, 248 | Payload: data, 249 | }) 250 | 251 | event, err := zstack.ReadEvent(ctx) 252 | assert.NoError(t, err) 253 | 254 | nodeLeave, ok := event.(zigbee.NodeLeaveEvent) 255 | 256 | assert.True(t, ok) 257 | 258 | assert.Equal(t, announce.SourceAddress, nodeLeave.NetworkAddress) 259 | assert.Equal(t, announce.IEEEAddress, nodeLeave.IEEEAddress) 260 | 261 | _, found := zstack.nodeTable.getByIEEE(announce.IEEEAddress) 262 | assert.False(t, found) 263 | }) 264 | 265 | t.Run("a new router will be queried for network state", func(t *testing.T) { 266 | unpiMock := unpiTest.NewMockAdapter() 267 | zstack := New(unpiMock, memory.New()) 268 | zstack.sem = semaphore.NewWeighted(8) 269 | defer unpiMock.Stop() 270 | defer unpiMock.AssertCalls(t) 271 | 272 | c := unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 273 | MessageType: SRSP, 274 | Subsystem: ZDO, 275 | CommandID: ZdoMGMTLQIReqReplyID, 276 | Payload: []byte{0x00}, 277 | }).Times(2) 278 | 279 | zstack.startNetworkManager() 280 | defer zstack.stopNetworkManager() 281 | 282 | time.Sleep(10 * time.Millisecond) 283 | 284 | announce := ZdoEndDeviceAnnceInd{ 285 | SourceAddress: zigbee.NetworkAddress(0x1000), 286 | NetworkAddress: zigbee.NetworkAddress(0x2000), 287 | IEEEAddress: zigbee.IEEEAddress(0x0102030405060708), 288 | Capabilities: ZdoEndDeviceAnnceIndCapabilities{ 289 | AltPANController: false, 290 | Router: true, 291 | PowerSource: false, 292 | ReceiveOnIdle: false, 293 | Reserved: 0, 294 | SecurityCapability: false, 295 | AddressAllocated: false, 296 | }, 297 | } 298 | 299 | data, _ := bytecodec.Marshal(announce) 300 | 301 | unpiMock.InjectOutgoing(Frame{ 302 | MessageType: AREQ, 303 | Subsystem: ZDO, 304 | CommandID: ZdoEndDeviceAnnceIndID, 305 | Payload: data, 306 | }) 307 | 308 | time.Sleep(20 * time.Millisecond) 309 | 310 | assert.Equal(t, 2, len(c.CapturedCalls)) 311 | 312 | frame := c.CapturedCalls[1] 313 | 314 | lqiReq := ZdoMGMTLQIReq{} 315 | _ = bytecodec.Unmarshal(frame.Frame.Payload, &lqiReq) 316 | 317 | assert.Equal(t, zigbee.NetworkAddress(0x2000), lqiReq.DestinationAddress) 318 | }) 319 | 320 | t.Run("nodes in lqi query are added to network manager", func(t *testing.T) { 321 | unpiMock := unpiTest.NewMockAdapter() 322 | zstack := New(unpiMock, memory.New()) 323 | zstack.sem = semaphore.NewWeighted(8) 324 | defer unpiMock.Stop() 325 | defer unpiMock.AssertCalls(t) 326 | 327 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 328 | MessageType: SRSP, 329 | Subsystem: ZDO, 330 | CommandID: ZdoMGMTLQIReqReplyID, 331 | Payload: []byte{0x00}, 332 | }).UnlimitedTimes() 333 | 334 | zstack.startNetworkManager() 335 | defer zstack.stopNetworkManager() 336 | 337 | time.Sleep(10 * time.Millisecond) 338 | 339 | announce := ZdoMGMTLQIRsp{ 340 | SourceAddress: 0, 341 | Status: 0, 342 | NeighbourTableEntries: 1, 343 | StartIndex: 0, 344 | Neighbors: []ZdoMGMTLQINeighbour{ 345 | { 346 | ExtendedPANID: zstack.NetworkProperties.ExtendedPANID, 347 | IEEEAddress: zigbee.IEEEAddress(0x1000), 348 | NetworkAddress: zigbee.NetworkAddress(0x2000), 349 | Status: ZdoMGMTLQINeighbourStatus{ 350 | Reserved: 0, 351 | Relationship: zigbee.RelationshipChild, 352 | RxOnWhenIdle: 0, 353 | DeviceType: zigbee.Router, 354 | }, 355 | PermitJoining: false, 356 | Depth: 1, 357 | LQI: 67, 358 | }, 359 | }, 360 | } 361 | 362 | data, _ := bytecodec.Marshal(announce) 363 | 364 | unpiMock.InjectOutgoing(Frame{ 365 | MessageType: AREQ, 366 | Subsystem: ZDO, 367 | CommandID: ZdoMGMTLQIRspID, 368 | Payload: data, 369 | }) 370 | 371 | time.Sleep(10 * time.Millisecond) 372 | 373 | node, found := zstack.nodeTable.getByIEEE(zigbee.IEEEAddress(0x1000)) 374 | assert.True(t, found) 375 | 376 | assert.Equal(t, zigbee.NetworkAddress(0x2000), node.NetworkAddress) 377 | assert.Equal(t, zigbee.Router, node.LogicalType) 378 | assert.Equal(t, uint8(0x43), node.LQI) 379 | assert.Equal(t, uint8(0x01), node.Depth) 380 | }) 381 | 382 | t.Run("nodes in lqi query are added to network manager", func(t *testing.T) { 383 | unpiMock := unpiTest.NewMockAdapter() 384 | zstack := New(unpiMock, memory.New()) 385 | zstack.sem = semaphore.NewWeighted(8) 386 | defer unpiMock.Stop() 387 | defer unpiMock.AssertCalls(t) 388 | 389 | zstack.nodeTable.addOrUpdate(zigbee.GenerateLocalAdministeredIEEEAddress(), 0x1122) 390 | 391 | lqiReqOn := unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 392 | MessageType: SRSP, 393 | Subsystem: ZDO, 394 | CommandID: ZdoMGMTLQIReqReplyID, 395 | Payload: []byte{0x00}, 396 | }).Times(1) 397 | 398 | lqiRespWithMore := ZdoMGMTLQIRsp{ 399 | SourceAddress: 0x1122, 400 | Status: 0, 401 | NeighbourTableEntries: 2, 402 | StartIndex: 0, 403 | Neighbors: []ZdoMGMTLQINeighbour{ 404 | { 405 | ExtendedPANID: zstack.NetworkProperties.ExtendedPANID, 406 | IEEEAddress: zigbee.IEEEAddress(0x1000), 407 | NetworkAddress: zigbee.NetworkAddress(0x2000), 408 | Status: ZdoMGMTLQINeighbourStatus{ 409 | Reserved: 0, 410 | Relationship: zigbee.RelationshipChild, 411 | RxOnWhenIdle: 0, 412 | DeviceType: zigbee.Router, 413 | }, 414 | PermitJoining: false, 415 | Depth: 1, 416 | LQI: 67, 417 | }, 418 | }, 419 | } 420 | 421 | zstack.processLQITable(lqiRespWithMore) 422 | 423 | assert.Equal(t, []byte{0x22, 0x11, 0x01}, lqiReqOn.CapturedCalls[0].Frame.Payload) 424 | }) 425 | 426 | t.Run("nodes in lqi query are not added if Ext PANID does not match", func(t *testing.T) { 427 | unpiMock := unpiTest.NewMockAdapter() 428 | zstack := New(unpiMock, memory.New()) 429 | zstack.sem = semaphore.NewWeighted(8) 430 | defer unpiMock.Stop() 431 | defer unpiMock.AssertCalls(t) 432 | 433 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 434 | MessageType: SRSP, 435 | Subsystem: ZDO, 436 | CommandID: ZdoMGMTLQIReqReplyID, 437 | Payload: []byte{0x00}, 438 | }).UnlimitedTimes() 439 | 440 | zstack.startNetworkManager() 441 | defer zstack.stopNetworkManager() 442 | 443 | time.Sleep(10 * time.Millisecond) 444 | 445 | announce := ZdoMGMTLQIRsp{ 446 | SourceAddress: 0, 447 | Status: 0, 448 | NeighbourTableEntries: 1, 449 | StartIndex: 0, 450 | Neighbors: []ZdoMGMTLQINeighbour{ 451 | { 452 | ExtendedPANID: 0xfffffff, 453 | IEEEAddress: zigbee.IEEEAddress(0x2000), 454 | NetworkAddress: zigbee.NetworkAddress(0x4000), 455 | Status: ZdoMGMTLQINeighbourStatus{ 456 | Reserved: 0, 457 | Relationship: zigbee.RelationshipParent, 458 | RxOnWhenIdle: 0, 459 | DeviceType: zigbee.Router, 460 | }, 461 | PermitJoining: false, 462 | Depth: 0, 463 | LQI: 67, 464 | }, 465 | }, 466 | } 467 | 468 | data, _ := bytecodec.Marshal(announce) 469 | 470 | unpiMock.InjectOutgoing(Frame{ 471 | MessageType: AREQ, 472 | Subsystem: ZDO, 473 | CommandID: ZdoMGMTLQIRspID, 474 | Payload: data, 475 | }) 476 | 477 | time.Sleep(10 * time.Millisecond) 478 | 479 | _, found := zstack.nodeTable.getByIEEE(zigbee.IEEEAddress(0x2000)) 480 | assert.False(t, found) 481 | }) 482 | 483 | t.Run("nodes in lqi query are not added if it has an invalid IEEE address", func(t *testing.T) { 484 | unpiMock := unpiTest.NewMockAdapter() 485 | zstack := New(unpiMock, memory.New()) 486 | zstack.sem = semaphore.NewWeighted(8) 487 | zstack.NetworkProperties.IEEEAddress = zigbee.IEEEAddress(1) 488 | 489 | defer unpiMock.Stop() 490 | defer unpiMock.AssertCalls(t) 491 | 492 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 493 | MessageType: SRSP, 494 | Subsystem: ZDO, 495 | CommandID: ZdoMGMTLQIReqReplyID, 496 | Payload: []byte{0x00}, 497 | }).UnlimitedTimes() 498 | 499 | zstack.startNetworkManager() 500 | defer zstack.stopNetworkManager() 501 | 502 | time.Sleep(10 * time.Millisecond) 503 | 504 | announce := ZdoMGMTLQIRsp{ 505 | SourceAddress: 0, 506 | Status: 0, 507 | NeighbourTableEntries: 1, 508 | StartIndex: 0, 509 | Neighbors: []ZdoMGMTLQINeighbour{ 510 | { 511 | ExtendedPANID: zstack.NetworkProperties.ExtendedPANID, 512 | IEEEAddress: zigbee.IEEEAddress(0), 513 | NetworkAddress: zigbee.NetworkAddress(0x4000), 514 | Status: ZdoMGMTLQINeighbourStatus{ 515 | Reserved: 0, 516 | Relationship: zigbee.RelationshipParent, 517 | RxOnWhenIdle: 0, 518 | DeviceType: zigbee.Router, 519 | }, 520 | PermitJoining: false, 521 | Depth: 0, 522 | LQI: 67, 523 | }, 524 | }, 525 | } 526 | 527 | data, _ := bytecodec.Marshal(announce) 528 | 529 | unpiMock.InjectOutgoing(Frame{ 530 | MessageType: AREQ, 531 | Subsystem: ZDO, 532 | CommandID: ZdoMGMTLQIRspID, 533 | Payload: data, 534 | }) 535 | 536 | time.Sleep(10 * time.Millisecond) 537 | 538 | _, found := zstack.nodeTable.getByIEEE(zigbee.IEEEAddress(0)) 539 | assert.False(t, found) 540 | }) 541 | 542 | t.Run("updates to the node table sends a node update event", func(t *testing.T) { 543 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 544 | defer cancel() 545 | 546 | unpiMock := unpiTest.NewMockAdapter() 547 | zstack := New(unpiMock, memory.New()) 548 | zstack.sem = semaphore.NewWeighted(8) 549 | defer unpiMock.Stop() 550 | defer unpiMock.AssertCalls(t) 551 | 552 | unpiMock.On(SREQ, ZDO, ZdoMGMTLQIReqID).Return(Frame{ 553 | MessageType: SRSP, 554 | Subsystem: ZDO, 555 | CommandID: ZdoMGMTLQIReqReplyID, 556 | Payload: []byte{0x00}, 557 | }).UnlimitedTimes() 558 | 559 | zstack.startNetworkManager() 560 | defer zstack.stopNetworkManager() 561 | 562 | time.Sleep(10 * time.Millisecond) 563 | 564 | go func() { 565 | time.Sleep(10 * time.Millisecond) 566 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(0x01), zigbee.NetworkAddress(0x02)) 567 | }() 568 | 569 | event, err := zstack.ReadEvent(ctx) 570 | assert.NoError(t, err) 571 | 572 | nodeUpdateEvent, ok := event.(zigbee.NodeUpdateEvent) 573 | 574 | assert.True(t, ok) 575 | 576 | assert.Equal(t, zigbee.IEEEAddress(0x01), nodeUpdateEvent.Node.IEEEAddress) 577 | assert.Equal(t, zigbee.NetworkAddress(0x02), nodeUpdateEvent.Node.NetworkAddress) 578 | }) 579 | } 580 | -------------------------------------------------------------------------------- /node_address.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/logwrap" 7 | "github.com/shimmeringbee/zigbee" 8 | ) 9 | 10 | func (z *ZStack) ResolveNodeIEEEAddress(ctx context.Context, address zigbee.NetworkAddress) (zigbee.IEEEAddress, error) { 11 | if node, found := z.nodeTable.getByNetwork(address); found { 12 | return node.IEEEAddress, nil 13 | } else { 14 | z.logger.LogDebug(ctx, "Asked to resolve Network Address to IEEE Address, but not present in node table, querying adapter.", logwrap.Datum("NetworkAddress", address)) 15 | return z.QueryNodeIEEEAddress(ctx, address) 16 | } 17 | } 18 | 19 | func (z *ZStack) ResolveNodeNWKAddress(ctx context.Context, address zigbee.IEEEAddress) (zigbee.NetworkAddress, error) { 20 | if node, found := z.nodeTable.getByIEEE(address); found { 21 | return node.NetworkAddress, nil 22 | } else { 23 | z.logger.LogDebug(ctx, "Asked to resolve IEEE Address to Network Address, but not present in node table, querying adapter.", logwrap.Datum("IEEEAddress", address.String())) 24 | return z.QueryNodeNWKAddress(ctx, address) 25 | } 26 | } 27 | 28 | func (z *ZStack) QueryNodeIEEEAddress(ctx context.Context, address zigbee.NetworkAddress) (zigbee.IEEEAddress, error) { 29 | if err := z.sem.Acquire(ctx, 1); err != nil { 30 | return zigbee.EmptyIEEEAddress, fmt.Errorf("failed to acquire semaphore: %w", err) 31 | } 32 | defer z.sem.Release(1) 33 | 34 | request := ZdoIEEEAddrReq{ 35 | NetworkAddress: address, 36 | ReqType: 0x00, 37 | StartIndex: 0x00, 38 | } 39 | 40 | resp, err := z.nodeRequest(ctx, &request, &ZdoIEEEAddrReqReply{}, &ZdoIEEEAddrRsp{}, func(i interface{}) bool { 41 | msg := i.(*ZdoIEEEAddrRsp) 42 | return msg.NetworkAddress == address 43 | }) 44 | 45 | castResp, ok := resp.(*ZdoIEEEAddrRsp) 46 | 47 | if ok { 48 | return castResp.IEEEAddress, nil 49 | } else { 50 | z.logger.LogError(ctx, "Failed to query adapter for IEEE Address.", logwrap.Datum("NetworkAddress", address), logwrap.Err(err)) 51 | return zigbee.EmptyIEEEAddress, err 52 | } 53 | } 54 | 55 | func (z *ZStack) QueryNodeNWKAddress(ctx context.Context, address zigbee.IEEEAddress) (zigbee.NetworkAddress, error) { 56 | if err := z.sem.Acquire(ctx, 1); err != nil { 57 | return zigbee.NetworkAddress(0x0), fmt.Errorf("failed to acquire semaphore: %w", err) 58 | } 59 | defer z.sem.Release(1) 60 | 61 | request := ZdoNWKAddrReq{ 62 | IEEEAddress: address, 63 | ReqType: 0x00, 64 | StartIndex: 0x00, 65 | } 66 | 67 | resp, err := z.nodeRequest(ctx, &request, &ZdoNWKAddrReqReply{}, &ZdoNWKAddrRsp{}, func(i interface{}) bool { 68 | msg := i.(*ZdoNWKAddrRsp) 69 | return msg.IEEEAddress == address 70 | }) 71 | 72 | castResp, ok := resp.(*ZdoNWKAddrRsp) 73 | 74 | if ok { 75 | return castResp.NetworkAddress, nil 76 | } else { 77 | z.logger.LogError(ctx, "Failed to query adapter for Network Address.", logwrap.Datum("IEEEAddress", address.String()), logwrap.Err(err)) 78 | return zigbee.NetworkAddress(0x0), err 79 | } 80 | } 81 | 82 | type ZdoIEEEAddrReq struct { 83 | NetworkAddress zigbee.NetworkAddress 84 | ReqType uint8 85 | StartIndex uint8 86 | } 87 | 88 | const ZdoIEEEAddrReqID uint8 = 0x01 89 | 90 | type ZdoIEEEAddrReqReply GenericZStackStatus 91 | 92 | func (s ZdoIEEEAddrReqReply) WasSuccessful() bool { 93 | return s.Status == ZSuccess 94 | } 95 | 96 | const ZdoIEEEAddrReqReplyID uint8 = 0x01 97 | 98 | type ZdoIEEEAddrRsp struct { 99 | Status ZStackStatus 100 | IEEEAddress zigbee.IEEEAddress 101 | NetworkAddress zigbee.NetworkAddress 102 | StartIndex uint8 103 | AssociatedDevices []zigbee.NetworkAddress `bcsliceprefix:"8"` 104 | } 105 | 106 | func (s ZdoIEEEAddrRsp) WasSuccessful() bool { 107 | return s.Status == ZSuccess 108 | } 109 | 110 | const ZdoIEEEAddrRspID uint8 = 0x81 111 | 112 | type ZdoNWKAddrReq struct { 113 | IEEEAddress zigbee.IEEEAddress 114 | ReqType uint8 115 | StartIndex uint8 116 | } 117 | 118 | const ZdoNWKAddrReqID uint8 = 0x00 119 | 120 | type ZdoNWKAddrReqReply GenericZStackStatus 121 | 122 | func (s ZdoNWKAddrReqReply) WasSuccessful() bool { 123 | return s.Status == ZSuccess 124 | } 125 | 126 | const ZdoNWKAddrReqReplyID uint8 = 0x00 127 | 128 | type ZdoNWKAddrRsp struct { 129 | Status ZStackStatus 130 | IEEEAddress zigbee.IEEEAddress 131 | NetworkAddress zigbee.NetworkAddress 132 | StartIndex uint8 133 | AssociatedDevices []zigbee.NetworkAddress `bcsliceprefix:"8"` 134 | } 135 | 136 | func (s ZdoNWKAddrRsp) WasSuccessful() bool { 137 | return s.Status == ZSuccess 138 | } 139 | 140 | const ZdoNWKAddrRspID uint8 = 0x80 141 | -------------------------------------------------------------------------------- /node_address_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_ResolveNodeIEEEAddress(t *testing.T) { 17 | t.Run("returns immediately if result is in cache, with no interaction unpi", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | defer unpiMock.AssertCalls(t) 23 | zstack := New(unpiMock, memory.New()) 24 | zstack.sem = semaphore.NewWeighted(8) 25 | defer unpiMock.Stop() 26 | 27 | zstack.nodeTable.addOrUpdate(0x1122334455667788, 0xaabb) 28 | 29 | ieee, err := zstack.ResolveNodeIEEEAddress(ctx, zigbee.NetworkAddress(0xaabb)) 30 | assert.NoError(t, err) 31 | assert.Equal(t, zigbee.IEEEAddress(0x1122334455667788), ieee) 32 | }) 33 | 34 | t.Run("returns result of successful call to QueryNodeIEEEAddress if not in cache", func(t *testing.T) { 35 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 36 | defer cancel() 37 | 38 | unpiMock := unpiTest.NewMockAdapter() 39 | defer unpiMock.AssertCalls(t) 40 | zstack := New(unpiMock, memory.New()) 41 | zstack.sem = semaphore.NewWeighted(8) 42 | defer unpiMock.Stop() 43 | 44 | call := unpiMock.On(SREQ, ZDO, ZdoIEEEAddrReqID).Return(Frame{ 45 | MessageType: SRSP, 46 | Subsystem: ZDO, 47 | CommandID: ZdoIEEEAddrReqReplyID, 48 | Payload: []byte{0x00}, 49 | }) 50 | 51 | go func() { 52 | time.Sleep(10 * time.Millisecond) 53 | unpiMock.InjectOutgoing(Frame{ 54 | MessageType: AREQ, 55 | Subsystem: ZDO, 56 | CommandID: ZdoIEEEAddrRspID, 57 | Payload: []byte{0x00, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x40, 0x00, 0x00}, 58 | }) 59 | }() 60 | 61 | ieee, err := zstack.ResolveNodeIEEEAddress(ctx, zigbee.NetworkAddress(0x4000)) 62 | assert.NoError(t, err) 63 | assert.Equal(t, zigbee.IEEEAddress(0x1122334455667788), ieee) 64 | 65 | addressReq := ZdoIEEEAddrReq{} 66 | bytecodec.Unmarshal(call.CapturedCalls[0].Frame.Payload, &addressReq) 67 | 68 | assert.Equal(t, zigbee.NetworkAddress(0x4000), addressReq.NetworkAddress) 69 | assert.Equal(t, uint8(0), addressReq.ReqType) 70 | assert.Equal(t, uint8(0), addressReq.StartIndex) 71 | }) 72 | } 73 | 74 | func Test_QueryNodeIEEEAddress(t *testing.T) { 75 | t.Run("returns an success on query, response for requested network address is received", func(t *testing.T) { 76 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 77 | defer cancel() 78 | 79 | unpiMock := unpiTest.NewMockAdapter() 80 | defer unpiMock.AssertCalls(t) 81 | zstack := New(unpiMock, memory.New()) 82 | zstack.sem = semaphore.NewWeighted(8) 83 | defer unpiMock.Stop() 84 | 85 | call := unpiMock.On(SREQ, ZDO, ZdoIEEEAddrReqID).Return(Frame{ 86 | MessageType: SRSP, 87 | Subsystem: ZDO, 88 | CommandID: ZdoIEEEAddrReqReplyID, 89 | Payload: []byte{0x00}, 90 | }) 91 | 92 | go func() { 93 | time.Sleep(10 * time.Millisecond) 94 | unpiMock.InjectOutgoing(Frame{ 95 | MessageType: AREQ, 96 | Subsystem: ZDO, 97 | CommandID: ZdoIEEEAddrRspID, 98 | Payload: []byte{0x00, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x40, 0x00, 0x00}, 99 | }) 100 | }() 101 | 102 | ieee, err := zstack.QueryNodeIEEEAddress(ctx, zigbee.NetworkAddress(0x4000)) 103 | assert.NoError(t, err) 104 | assert.Equal(t, zigbee.IEEEAddress(0x1122334455667788), ieee) 105 | 106 | addressReq := ZdoIEEEAddrReq{} 107 | bytecodec.Unmarshal(call.CapturedCalls[0].Frame.Payload, &addressReq) 108 | 109 | assert.Equal(t, zigbee.NetworkAddress(0x4000), addressReq.NetworkAddress) 110 | assert.Equal(t, uint8(0), addressReq.ReqType) 111 | assert.Equal(t, uint8(0), addressReq.StartIndex) 112 | }) 113 | } 114 | 115 | func Test_ResolveNodeNWKAddress(t *testing.T) { 116 | t.Run("returns immediately if result is in cache, with no interaction unpi", func(t *testing.T) { 117 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 118 | defer cancel() 119 | 120 | unpiMock := unpiTest.NewMockAdapter() 121 | defer unpiMock.AssertCalls(t) 122 | zstack := New(unpiMock, memory.New()) 123 | zstack.sem = semaphore.NewWeighted(8) 124 | defer unpiMock.Stop() 125 | 126 | zstack.nodeTable.addOrUpdate(0x1122334455667788, 0xaabb) 127 | 128 | nwk, err := zstack.ResolveNodeNWKAddress(ctx, zigbee.IEEEAddress(0x1122334455667788)) 129 | assert.NoError(t, err) 130 | assert.Equal(t, zigbee.NetworkAddress(0xaabb), nwk) 131 | }) 132 | 133 | t.Run("returns result of successful call to QueryNodeNWKAddress if not in cache", func(t *testing.T) { 134 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 135 | defer cancel() 136 | 137 | unpiMock := unpiTest.NewMockAdapter() 138 | defer unpiMock.AssertCalls(t) 139 | zstack := New(unpiMock, memory.New()) 140 | zstack.sem = semaphore.NewWeighted(8) 141 | defer unpiMock.Stop() 142 | 143 | call := unpiMock.On(SREQ, ZDO, ZdoNWKAddrReqID).Return(Frame{ 144 | MessageType: SRSP, 145 | Subsystem: ZDO, 146 | CommandID: ZdoNWKAddrReqReplyID, 147 | Payload: []byte{0x00}, 148 | }) 149 | 150 | go func() { 151 | time.Sleep(10 * time.Millisecond) 152 | unpiMock.InjectOutgoing(Frame{ 153 | MessageType: AREQ, 154 | Subsystem: ZDO, 155 | CommandID: ZdoNWKAddrRspID, 156 | Payload: []byte{0x00, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x40, 0x00, 0x00}, 157 | }) 158 | }() 159 | 160 | NWK, err := zstack.ResolveNodeNWKAddress(ctx, zigbee.IEEEAddress(0x1122334455667788)) 161 | assert.NoError(t, err) 162 | assert.Equal(t, zigbee.NetworkAddress(0x4000), NWK) 163 | 164 | addressReq := ZdoNWKAddrReq{} 165 | bytecodec.Unmarshal(call.CapturedCalls[0].Frame.Payload, &addressReq) 166 | 167 | assert.Equal(t, zigbee.IEEEAddress(0x1122334455667788), addressReq.IEEEAddress) 168 | assert.Equal(t, uint8(0), addressReq.ReqType) 169 | assert.Equal(t, uint8(0), addressReq.StartIndex) 170 | }) 171 | } 172 | 173 | func Test_QueryNodeNWKAddress(t *testing.T) { 174 | t.Run("returns an success on query, response for requested network address is received", func(t *testing.T) { 175 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 176 | defer cancel() 177 | 178 | unpiMock := unpiTest.NewMockAdapter() 179 | defer unpiMock.AssertCalls(t) 180 | zstack := New(unpiMock, memory.New()) 181 | zstack.sem = semaphore.NewWeighted(8) 182 | defer unpiMock.Stop() 183 | 184 | call := unpiMock.On(SREQ, ZDO, ZdoNWKAddrReqID).Return(Frame{ 185 | MessageType: SRSP, 186 | Subsystem: ZDO, 187 | CommandID: ZdoNWKAddrReqReplyID, 188 | Payload: []byte{0x00}, 189 | }) 190 | 191 | go func() { 192 | time.Sleep(10 * time.Millisecond) 193 | unpiMock.InjectOutgoing(Frame{ 194 | MessageType: AREQ, 195 | Subsystem: ZDO, 196 | CommandID: ZdoNWKAddrRspID, 197 | Payload: []byte{0x00, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x40, 0x00, 0x00}, 198 | }) 199 | }() 200 | 201 | ieee, err := zstack.QueryNodeNWKAddress(ctx, zigbee.IEEEAddress(0x1122334455667788)) 202 | assert.NoError(t, err) 203 | assert.Equal(t, zigbee.NetworkAddress(0x4000), ieee) 204 | 205 | addressReq := ZdoNWKAddrReq{} 206 | bytecodec.Unmarshal(call.CapturedCalls[0].Frame.Payload, &addressReq) 207 | 208 | assert.Equal(t, zigbee.IEEEAddress(0x1122334455667788), addressReq.IEEEAddress) 209 | assert.Equal(t, uint8(0), addressReq.ReqType) 210 | assert.Equal(t, uint8(0), addressReq.StartIndex) 211 | }) 212 | } 213 | 214 | func Test_IEEEMessages(t *testing.T) { 215 | t.Run("verify ZdoIEEEAddrReq marshals", func(t *testing.T) { 216 | req := ZdoIEEEAddrReq{ 217 | NetworkAddress: 0x2040, 218 | ReqType: 0x01, 219 | StartIndex: 0x02, 220 | } 221 | 222 | data, err := bytecodec.Marshal(req) 223 | 224 | assert.NoError(t, err) 225 | assert.Equal(t, []byte{0x40, 0x20, 0x01, 0x02}, data) 226 | }) 227 | 228 | t.Run("verify ZdoIEEEAddrReqReply marshals", func(t *testing.T) { 229 | req := ZdoIEEEAddrReqReply{ 230 | Status: 1, 231 | } 232 | 233 | data, err := bytecodec.Marshal(req) 234 | 235 | assert.NoError(t, err) 236 | assert.Equal(t, []byte{0x01}, data) 237 | }) 238 | 239 | t.Run("ZdoIEEEAddrReqReply returns true if success", func(t *testing.T) { 240 | g := ZdoIEEEAddrReqReply{Status: ZSuccess} 241 | assert.True(t, g.WasSuccessful()) 242 | }) 243 | 244 | t.Run("ZdoIEEEAddrReqReply returns false if not success", func(t *testing.T) { 245 | g := ZdoIEEEAddrReqReply{Status: ZFailure} 246 | assert.False(t, g.WasSuccessful()) 247 | }) 248 | 249 | t.Run("verify ZdoIEEEAddrRsp marshals", func(t *testing.T) { 250 | req := ZdoIEEEAddrRsp{ 251 | Status: 0x01, 252 | IEEEAddress: 0x1122334455667788, 253 | NetworkAddress: 0xaabb, 254 | StartIndex: 0x02, 255 | AssociatedDevices: []zigbee.NetworkAddress{0x2002, 0x3003}, 256 | } 257 | 258 | data, err := bytecodec.Marshal(req) 259 | 260 | assert.NoError(t, err) 261 | assert.Equal(t, []byte{0x01, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0xbb, 0xaa, 0x02, 0x02, 0x02, 0x20, 0x03, 0x30}, data) 262 | }) 263 | 264 | t.Run("ZdoIEEEAddrRsp returns true if success", func(t *testing.T) { 265 | g := ZdoIEEEAddrRsp{Status: ZSuccess} 266 | assert.True(t, g.WasSuccessful()) 267 | }) 268 | 269 | t.Run("ZdoIEEEAddrRsp returns false if not success", func(t *testing.T) { 270 | g := ZdoIEEEAddrRsp{Status: ZFailure} 271 | assert.False(t, g.WasSuccessful()) 272 | }) 273 | } 274 | 275 | func Test_NWKMessages(t *testing.T) { 276 | t.Run("verify ZdoNWKAddrReq marshals", func(t *testing.T) { 277 | req := ZdoNWKAddrReq{ 278 | IEEEAddress: 0x1122334455667788, 279 | ReqType: 0x01, 280 | StartIndex: 0x02, 281 | } 282 | 283 | data, err := bytecodec.Marshal(req) 284 | 285 | assert.NoError(t, err) 286 | assert.Equal(t, []byte{0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x01, 0x02}, data) 287 | }) 288 | 289 | t.Run("verify ZdoNWKAddrReqReply marshals", func(t *testing.T) { 290 | req := ZdoNWKAddrReqReply{ 291 | Status: 1, 292 | } 293 | 294 | data, err := bytecodec.Marshal(req) 295 | 296 | assert.NoError(t, err) 297 | assert.Equal(t, []byte{0x01}, data) 298 | }) 299 | 300 | t.Run("ZdoNWKAddrReqReply returns true if success", func(t *testing.T) { 301 | g := ZdoNWKAddrReqReply{Status: ZSuccess} 302 | assert.True(t, g.WasSuccessful()) 303 | }) 304 | 305 | t.Run("ZdoNWKAddrReqReply returns false if not success", func(t *testing.T) { 306 | g := ZdoNWKAddrReqReply{Status: ZFailure} 307 | assert.False(t, g.WasSuccessful()) 308 | }) 309 | 310 | t.Run("verify ZdoNWKAddrRsp marshals", func(t *testing.T) { 311 | req := ZdoNWKAddrRsp{ 312 | Status: 0x01, 313 | IEEEAddress: 0x1122334455667788, 314 | NetworkAddress: 0xaabb, 315 | StartIndex: 0x02, 316 | AssociatedDevices: []zigbee.NetworkAddress{0x2002, 0x3003}, 317 | } 318 | 319 | data, err := bytecodec.Marshal(req) 320 | 321 | assert.NoError(t, err) 322 | assert.Equal(t, []byte{0x01, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0xbb, 0xaa, 0x02, 0x02, 0x02, 0x20, 0x03, 0x30}, data) 323 | }) 324 | 325 | t.Run("ZdoNWKAddrRsp returns true if success", func(t *testing.T) { 326 | g := ZdoNWKAddrRsp{Status: ZSuccess} 327 | assert.True(t, g.WasSuccessful()) 328 | }) 329 | 330 | t.Run("ZdoNWKAddrRsp returns false if not success", func(t *testing.T) { 331 | g := ZdoNWKAddrRsp{Status: ZFailure} 332 | assert.False(t, g.WasSuccessful()) 333 | }) 334 | } 335 | -------------------------------------------------------------------------------- /node_bind.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) BindNodeToController(ctx context.Context, nodeAddress zigbee.IEEEAddress, sourceEndpoint zigbee.Endpoint, destinationEndpoint zigbee.Endpoint, cluster zigbee.ClusterID) error { 10 | networkAddress, err := z.ResolveNodeNWKAddress(ctx, nodeAddress) 11 | if err != nil { 12 | return nil 13 | } 14 | 15 | if err := z.sem.Acquire(ctx, 1); err != nil { 16 | return fmt.Errorf("failed to acquire semaphore: %w", err) 17 | } 18 | defer z.sem.Release(1) 19 | 20 | request := ZdoBindReq{ 21 | TargetAddress: networkAddress, 22 | SourceAddress: nodeAddress, 23 | SourceEndpoint: sourceEndpoint, 24 | ClusterID: cluster, 25 | DestinationAddressMode: 0x03, // IEEE Address (64 bits) 26 | DestinationAddress: uint64(z.NetworkProperties.IEEEAddress), 27 | DestinationEndpoint: destinationEndpoint, 28 | } 29 | 30 | _, err = z.nodeRequest(ctx, &request, &ZdoBindReqReply{}, &ZdoBindRsp{}, func(i interface{}) bool { 31 | msg := i.(*ZdoBindRsp) 32 | return msg.SourceAddress == networkAddress 33 | }) 34 | 35 | return err 36 | } 37 | 38 | type ZdoBindReq struct { 39 | TargetAddress zigbee.NetworkAddress 40 | SourceAddress zigbee.IEEEAddress 41 | SourceEndpoint zigbee.Endpoint 42 | ClusterID zigbee.ClusterID 43 | DestinationAddressMode uint8 44 | DestinationAddress uint64 45 | DestinationEndpoint zigbee.Endpoint 46 | } 47 | 48 | const ZdoBindReqID uint8 = 0x21 49 | 50 | type ZdoBindReqReply GenericZStackStatus 51 | 52 | func (r ZdoBindReqReply) WasSuccessful() bool { 53 | return r.Status == ZSuccess 54 | } 55 | 56 | const ZdoBindReqReplyID uint8 = 0x21 57 | 58 | type ZdoBindRsp struct { 59 | SourceAddress zigbee.NetworkAddress 60 | Status ZStackStatus 61 | } 62 | 63 | func (r ZdoBindRsp) WasSuccessful() bool { 64 | return r.Status == ZSuccess 65 | } 66 | 67 | const ZdoBindRspID uint8 = 0xa1 68 | -------------------------------------------------------------------------------- /node_bind_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_BindToNode(t *testing.T) { 17 | t.Run("returns an success on query, response for requested network address is received", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | zstack := New(unpiMock, memory.New()) 23 | zstack.sem = semaphore.NewWeighted(8) 24 | defer unpiMock.Stop() 25 | 26 | call := unpiMock.On(SREQ, ZDO, ZdoBindReqReplyID).Return(Frame{ 27 | MessageType: SRSP, 28 | Subsystem: ZDO, 29 | CommandID: ZdoBindReqReplyID, 30 | Payload: []byte{0x00}, 31 | }) 32 | 33 | go func() { 34 | time.Sleep(10 * time.Millisecond) 35 | unpiMock.InjectOutgoing(Frame{ 36 | MessageType: AREQ, 37 | Subsystem: ZDO, 38 | CommandID: ZdoBindRspID, 39 | Payload: []byte{0x00, 0x40, 0x00}, 40 | }) 41 | }() 42 | 43 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(1), zigbee.NetworkAddress(0x4000)) 44 | zstack.NetworkProperties.IEEEAddress = zigbee.IEEEAddress(0xaa) 45 | 46 | err := zstack.BindNodeToController(ctx, zigbee.IEEEAddress(1), 2, 4, 5) 47 | assert.NoError(t, err) 48 | 49 | bindReq := ZdoBindReq{} 50 | bytecodec.Unmarshal(call.CapturedCalls[0].Frame.Payload, &bindReq) 51 | 52 | assert.Equal(t, zigbee.NetworkAddress(0x4000), bindReq.TargetAddress) 53 | assert.Equal(t, zigbee.IEEEAddress(1), bindReq.SourceAddress) 54 | assert.Equal(t, zigbee.Endpoint(2), bindReq.SourceEndpoint) 55 | assert.Equal(t, uint64(0xaa), bindReq.DestinationAddress) 56 | assert.Equal(t, zigbee.Endpoint(4), bindReq.DestinationEndpoint) 57 | assert.Equal(t, zigbee.ClusterID(0x5), bindReq.ClusterID) 58 | assert.Equal(t, uint8(0x03), bindReq.DestinationAddressMode) 59 | 60 | unpiMock.AssertCalls(t) 61 | }) 62 | } 63 | 64 | func Test_BindMessages(t *testing.T) { 65 | t.Run("verify ZdoBindReq marshals", func(t *testing.T) { 66 | req := ZdoBindReq{ 67 | TargetAddress: 0x2021, 68 | SourceAddress: zigbee.IEEEAddress(0x8899aabbccddeeff), 69 | SourceEndpoint: 0x01, 70 | ClusterID: 0xcafe, 71 | DestinationAddressMode: 0x01, 72 | DestinationAddress: 0x3ffe, 73 | DestinationEndpoint: 0x02, 74 | } 75 | 76 | data, err := bytecodec.Marshal(req) 77 | 78 | assert.NoError(t, err) 79 | assert.Equal(t, []byte{0x21, 0x20, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x01, 0xfe, 0xca, 0x01, 0xfe, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, data) 80 | }) 81 | 82 | t.Run("verify ZdoBindReqReply marshals", func(t *testing.T) { 83 | req := ZdoBindReqReply{ 84 | Status: 1, 85 | } 86 | 87 | data, err := bytecodec.Marshal(req) 88 | 89 | assert.NoError(t, err) 90 | assert.Equal(t, []byte{0x01}, data) 91 | }) 92 | 93 | t.Run("ZdoBindReqReply returns true if success", func(t *testing.T) { 94 | g := ZdoBindReqReply{Status: ZSuccess} 95 | assert.True(t, g.WasSuccessful()) 96 | }) 97 | 98 | t.Run("ZdoBindReqReply returns false if not success", func(t *testing.T) { 99 | g := ZdoBindReqReply{Status: ZFailure} 100 | assert.False(t, g.WasSuccessful()) 101 | }) 102 | 103 | t.Run("verify ZdoBindRsp marshals", func(t *testing.T) { 104 | req := ZdoBindRsp{ 105 | SourceAddress: zigbee.NetworkAddress(0x2000), 106 | Status: 1, 107 | } 108 | 109 | data, err := bytecodec.Marshal(req) 110 | 111 | assert.NoError(t, err) 112 | assert.Equal(t, []byte{0x00, 0x20, 0x01}, data) 113 | }) 114 | 115 | t.Run("ZdoBindRsp returns true if success", func(t *testing.T) { 116 | g := ZdoBindRsp{Status: ZSuccess} 117 | assert.True(t, g.WasSuccessful()) 118 | }) 119 | 120 | t.Run("ZdoBindRsp returns false if not success", func(t *testing.T) { 121 | g := ZdoBindRsp{Status: ZFailure} 122 | assert.False(t, g.WasSuccessful()) 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /node_description.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) QueryNodeDescription(ctx context.Context, ieeeAddress zigbee.IEEEAddress) (zigbee.NodeDescription, error) { 10 | nwkAddress, err := z.ResolveNodeNWKAddress(ctx, ieeeAddress) 11 | if err != nil { 12 | return zigbee.NodeDescription{}, err 13 | } 14 | 15 | if err := z.sem.Acquire(ctx, 1); err != nil { 16 | return zigbee.NodeDescription{}, fmt.Errorf("failed to acquire semaphore: %w", err) 17 | } 18 | defer z.sem.Release(1) 19 | 20 | request := ZdoNodeDescReq{ 21 | DestinationAddress: nwkAddress, 22 | OfInterestAddress: nwkAddress, 23 | } 24 | 25 | resp, err := z.nodeRequest(ctx, &request, &ZdoNodeDescReqReply{}, &ZdoNodeDescRsp{}, func(i interface{}) bool { 26 | msg := i.(*ZdoNodeDescRsp) 27 | return msg.OfInterestAddress == nwkAddress 28 | }) 29 | 30 | castResp, ok := resp.(*ZdoNodeDescRsp) 31 | 32 | if ok { 33 | return zigbee.NodeDescription{ 34 | LogicalType: castResp.Capabilities.LogicalType, 35 | ManufacturerCode: zigbee.ManufacturerCode(castResp.ManufacturerCode), 36 | }, nil 37 | } else { 38 | return zigbee.NodeDescription{}, err 39 | } 40 | } 41 | 42 | type ZdoNodeDescReq struct { 43 | DestinationAddress zigbee.NetworkAddress 44 | OfInterestAddress zigbee.NetworkAddress 45 | } 46 | 47 | const ZdoNodeDescReqID uint8 = 0x02 48 | 49 | type ZdoNodeDescReqReply GenericZStackStatus 50 | 51 | func (r ZdoNodeDescReqReply) WasSuccessful() bool { 52 | return r.Status == ZSuccess 53 | } 54 | 55 | const ZdoNodeDescReqReplyID uint8 = 0x02 56 | 57 | type ZdoNodeDescRspCapabilities struct { 58 | Reserved uint8 `bcfieldwidth:"3"` 59 | UserDescriptorAvailable bool `bcfieldwidth:"1"` 60 | ComplexDescriptorAvailable bool `bcfieldwidth:"1"` 61 | LogicalType zigbee.LogicalType `bcfieldwidth:"3"` 62 | } 63 | 64 | type ZdoNodeDescRspServerMask struct { 65 | Reserved0 uint8 `bcfieldwidth:"8"` 66 | Reserved1 uint8 `bcfieldwidth:"2"` 67 | BackupDiscoveryCache bool `bcfieldwidth:"1"` 68 | PrimaryDiscoveryCache bool `bcfieldwidth:"1"` 69 | BackupBindingTableCache bool `bcfieldwidth:"1"` 70 | PrimaryBindingTableCache bool `bcfieldwidth:"1"` 71 | BackupTrustCenter bool `bcfieldwidth:"1"` 72 | PrimaryTrustCenter bool `bcfieldwidth:"1"` 73 | } 74 | 75 | type ZdoNodeDescRsp struct { 76 | SourceAddress zigbee.NetworkAddress 77 | Status ZStackStatus 78 | OfInterestAddress zigbee.NetworkAddress 79 | Capabilities ZdoNodeDescRspCapabilities 80 | NodeFrequencyBand uint8 `bcfieldwidth:"3"` 81 | APSFlags uint8 `bcfieldwidth:"5"` 82 | MacCapabilitiesFlags uint8 83 | ManufacturerCode uint16 84 | MaxBufferSize uint8 85 | MaxInTransferSize uint16 86 | ServerMask ZdoNodeDescRspServerMask 87 | MaxOutTransferSize uint16 88 | DescriptorCapabilities uint8 89 | } 90 | 91 | func (r ZdoNodeDescRsp) WasSuccessful() bool { 92 | return r.Status == ZSuccess 93 | } 94 | 95 | const ZdoNodeDescRspID uint8 = 0x82 96 | -------------------------------------------------------------------------------- /node_description_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_QueryNodeDescription(t *testing.T) { 17 | t.Run("returns an success on query, response for requested network address is received", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | zstack := New(unpiMock, memory.New()) 23 | zstack.sem = semaphore.NewWeighted(8) 24 | defer unpiMock.Stop() 25 | 26 | unpiMock.On(SREQ, ZDO, ZdoNodeDescReqID).Return(Frame{ 27 | MessageType: SRSP, 28 | Subsystem: ZDO, 29 | CommandID: ZdoNodeDescReqReplyID, 30 | Payload: []byte{0x00}, 31 | }) 32 | 33 | go func() { 34 | time.Sleep(10 * time.Millisecond) 35 | unpiMock.InjectOutgoing(Frame{ 36 | MessageType: AREQ, 37 | Subsystem: ZDO, 38 | CommandID: ZdoNodeDescRspID, 39 | Payload: []byte{0x00, 0x20, 0x01, 0x00, 0x40, 0x02, 0x02, 0x03, 0x05, 0x04, 0x06, 0x08, 0x07, 0x0a, 0x09, 0x0c, 0x0b, 0x0d}, 40 | }) 41 | }() 42 | 43 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(0x11223344556677), zigbee.NetworkAddress(0x4000)) 44 | 45 | nodeDescription, err := zstack.QueryNodeDescription(ctx, zigbee.IEEEAddress(0x11223344556677)) 46 | assert.NoError(t, err) 47 | assert.Equal(t, zigbee.NodeDescription{ 48 | LogicalType: zigbee.EndDevice, 49 | ManufacturerCode: 0x0405, 50 | }, nodeDescription) 51 | 52 | unpiMock.AssertCalls(t) 53 | }) 54 | } 55 | 56 | func Test_NodeDescriptionMessages(t *testing.T) { 57 | t.Run("verify ZdoNodeDescReq marshals", func(t *testing.T) { 58 | req := ZdoNodeDescReq{ 59 | DestinationAddress: zigbee.NetworkAddress(0x2000), 60 | OfInterestAddress: zigbee.NetworkAddress(0x4000), 61 | } 62 | 63 | data, err := bytecodec.Marshal(req) 64 | 65 | assert.NoError(t, err) 66 | assert.Equal(t, []byte{0x00, 0x20, 0x00, 0x40}, data) 67 | }) 68 | 69 | t.Run("verify ZdoNodeDescReqReply marshals", func(t *testing.T) { 70 | req := ZdoNodeDescReqReply{ 71 | Status: 1, 72 | } 73 | 74 | data, err := bytecodec.Marshal(req) 75 | 76 | assert.NoError(t, err) 77 | assert.Equal(t, []byte{0x01}, data) 78 | }) 79 | 80 | t.Run("ZdoNodeDescReqReply returns true if success", func(t *testing.T) { 81 | g := ZdoNodeDescReqReply{Status: ZSuccess} 82 | assert.True(t, g.WasSuccessful()) 83 | }) 84 | 85 | t.Run("ZdoNodeDescReqReply returns false if not success", func(t *testing.T) { 86 | g := ZdoNodeDescReqReply{Status: ZFailure} 87 | assert.False(t, g.WasSuccessful()) 88 | }) 89 | 90 | t.Run("verify ZdoNodeDescRsp marshals", func(t *testing.T) { 91 | req := ZdoNodeDescRsp{ 92 | SourceAddress: zigbee.NetworkAddress(0x2000), 93 | Status: 1, 94 | OfInterestAddress: zigbee.NetworkAddress(0x4000), 95 | Capabilities: ZdoNodeDescRspCapabilities{ 96 | LogicalType: zigbee.Router, 97 | }, 98 | NodeFrequencyBand: 0x00, 99 | APSFlags: 0x02, 100 | MacCapabilitiesFlags: 0x03, 101 | ManufacturerCode: 0x0405, 102 | MaxBufferSize: 0x06, 103 | MaxInTransferSize: 0x0708, 104 | ServerMask: ZdoNodeDescRspServerMask{ 105 | Reserved0: 0, 106 | Reserved1: 0, 107 | BackupDiscoveryCache: false, 108 | PrimaryDiscoveryCache: false, 109 | BackupBindingTableCache: false, 110 | PrimaryBindingTableCache: false, 111 | BackupTrustCenter: true, 112 | PrimaryTrustCenter: false, 113 | }, 114 | MaxOutTransferSize: 0x0b0c, 115 | DescriptorCapabilities: 0x0d, 116 | } 117 | 118 | data, err := bytecodec.Marshal(req) 119 | 120 | assert.NoError(t, err) 121 | assert.Equal(t, []byte{0x00, 0x20, 0x01, 0x00, 0x40, 0x01, 0x02, 0x03, 0x05, 0x04, 0x06, 0x08, 0x07, 0x00, 0x02, 0x0c, 0x0b, 0x0d}, data) 122 | }) 123 | 124 | t.Run("ZdoNodeDescRsp returns true if success", func(t *testing.T) { 125 | g := ZdoNodeDescRsp{Status: ZSuccess} 126 | assert.True(t, g.WasSuccessful()) 127 | }) 128 | 129 | t.Run("ZdoNodeDescRsp returns false if not success", func(t *testing.T) { 130 | g := ZdoNodeDescRsp{Status: ZFailure} 131 | assert.False(t, g.WasSuccessful()) 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /node_endpoint_description.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) QueryNodeEndpointDescription(ctx context.Context, ieeeAddress zigbee.IEEEAddress, endpoint zigbee.Endpoint) (zigbee.EndpointDescription, error) { 10 | networkAddress, err := z.ResolveNodeNWKAddress(ctx, ieeeAddress) 11 | if err != nil { 12 | return zigbee.EndpointDescription{}, err 13 | } 14 | 15 | if err := z.sem.Acquire(ctx, 1); err != nil { 16 | return zigbee.EndpointDescription{}, fmt.Errorf("failed to acquire semaphore: %w", err) 17 | } 18 | defer z.sem.Release(1) 19 | 20 | request := ZdoSimpleDescReq{ 21 | DestinationAddress: networkAddress, 22 | OfInterestAddress: networkAddress, 23 | Endpoint: endpoint, 24 | } 25 | 26 | resp, err := z.nodeRequest(ctx, &request, &ZdoSimpleDescReqReply{}, &ZdoSimpleDescRsp{}, func(i interface{}) bool { 27 | msg := i.(*ZdoSimpleDescRsp) 28 | return msg.OfInterestAddress == networkAddress && msg.Endpoint == endpoint 29 | }) 30 | 31 | castResp, ok := resp.(*ZdoSimpleDescRsp) 32 | 33 | if ok { 34 | return zigbee.EndpointDescription{ 35 | Endpoint: castResp.Endpoint, 36 | ProfileID: castResp.ProfileID, 37 | DeviceID: castResp.DeviceID, 38 | DeviceVersion: castResp.DeviceVersion, 39 | InClusterList: castResp.InClusterList, 40 | OutClusterList: castResp.OutClusterList, 41 | }, nil 42 | } else { 43 | return zigbee.EndpointDescription{}, err 44 | } 45 | } 46 | 47 | type ZdoSimpleDescReq struct { 48 | DestinationAddress zigbee.NetworkAddress 49 | OfInterestAddress zigbee.NetworkAddress 50 | Endpoint zigbee.Endpoint 51 | } 52 | 53 | const ZdoSimpleDescReqID uint8 = 0x04 54 | 55 | type ZdoSimpleDescReqReply GenericZStackStatus 56 | 57 | func (r ZdoSimpleDescReqReply) WasSuccessful() bool { 58 | return r.Status == ZSuccess 59 | } 60 | 61 | const ZdoSimpleDescReqReplyID uint8 = 0x04 62 | 63 | type ZdoSimpleDescRsp struct { 64 | SourceAddress zigbee.NetworkAddress 65 | Status ZStackStatus 66 | OfInterestAddress zigbee.NetworkAddress 67 | Length uint8 68 | Endpoint zigbee.Endpoint 69 | ProfileID zigbee.ProfileID 70 | DeviceID uint16 71 | DeviceVersion uint8 72 | InClusterList []zigbee.ClusterID `bcsliceprefix:"8"` 73 | OutClusterList []zigbee.ClusterID `bcsliceprefix:"8"` 74 | } 75 | 76 | func (r ZdoSimpleDescRsp) WasSuccessful() bool { 77 | return r.Status == ZSuccess 78 | } 79 | 80 | const ZdoSimpleDescRspID uint8 = 0x84 81 | -------------------------------------------------------------------------------- /node_endpoint_description_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_QueryNodeEndpointDescription(t *testing.T) { 17 | t.Run("returns an success on query, response for requested network address is received", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | zstack := New(unpiMock, memory.New()) 23 | zstack.sem = semaphore.NewWeighted(8) 24 | defer unpiMock.Stop() 25 | 26 | unpiMock.On(SREQ, ZDO, ZdoSimpleDescReqID).Return(Frame{ 27 | MessageType: SRSP, 28 | Subsystem: ZDO, 29 | CommandID: ZdoSimpleDescReqReplyID, 30 | Payload: []byte{0x00}, 31 | }) 32 | 33 | go func() { 34 | time.Sleep(10 * time.Millisecond) 35 | unpiMock.InjectOutgoing(Frame{ 36 | MessageType: AREQ, 37 | Subsystem: ZDO, 38 | CommandID: ZdoSimpleDescRspID, 39 | Payload: []byte{0x00, 0x20, 0x00, 0x00, 0x40, 0xff, 0x01, 0x01, 0x01, 0x01, 0x00, 0x02, 0x01, 0x01, 0x00, 0x01, 0x02, 0x00}, 40 | }) 41 | }() 42 | 43 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(0x11223344556677), zigbee.NetworkAddress(0x4000)) 44 | 45 | endpoints, err := zstack.QueryNodeEndpointDescription(ctx, zigbee.IEEEAddress(0x11223344556677), 0x01) 46 | assert.NoError(t, err) 47 | assert.Equal(t, zigbee.EndpointDescription{ 48 | Endpoint: 0x01, 49 | ProfileID: 0x0101, 50 | DeviceID: 1, 51 | DeviceVersion: 2, 52 | InClusterList: []zigbee.ClusterID{0x0001}, 53 | OutClusterList: []zigbee.ClusterID{0x0002}, 54 | }, endpoints) 55 | 56 | unpiMock.AssertCalls(t) 57 | }) 58 | } 59 | 60 | func Test_EndpointDescriptionMessages(t *testing.T) { 61 | t.Run("verify ZdoSimpleDescReq marshals", func(t *testing.T) { 62 | req := ZdoSimpleDescReq{ 63 | DestinationAddress: zigbee.NetworkAddress(0x2000), 64 | OfInterestAddress: zigbee.NetworkAddress(0x4000), 65 | Endpoint: 0x08, 66 | } 67 | 68 | data, err := bytecodec.Marshal(req) 69 | 70 | assert.NoError(t, err) 71 | assert.Equal(t, []byte{0x00, 0x20, 0x00, 0x40, 0x08}, data) 72 | }) 73 | 74 | t.Run("verify ZdoSimpleDescReqReply marshals", func(t *testing.T) { 75 | req := ZdoSimpleDescReqReply{ 76 | Status: 1, 77 | } 78 | 79 | data, err := bytecodec.Marshal(req) 80 | 81 | assert.NoError(t, err) 82 | assert.Equal(t, []byte{0x01}, data) 83 | }) 84 | 85 | t.Run("ZdoSimpleDescReqReply returns true if success", func(t *testing.T) { 86 | g := ZdoSimpleDescReqReply{Status: ZSuccess} 87 | assert.True(t, g.WasSuccessful()) 88 | }) 89 | 90 | t.Run("ZdoSimpleDescReqReply returns false if not success", func(t *testing.T) { 91 | g := ZdoSimpleDescReqReply{Status: ZFailure} 92 | assert.False(t, g.WasSuccessful()) 93 | }) 94 | 95 | t.Run("verify ZdoSimpleDescRsp marshals", func(t *testing.T) { 96 | req := ZdoSimpleDescRsp{ 97 | SourceAddress: zigbee.NetworkAddress(0x2000), 98 | Status: 1, 99 | OfInterestAddress: zigbee.NetworkAddress(0x4000), 100 | Length: 0x0a, 101 | Endpoint: 0x08, 102 | ProfileID: 0x1234, 103 | DeviceID: 0x5678, 104 | DeviceVersion: 0, 105 | InClusterList: []zigbee.ClusterID{0x1234}, 106 | OutClusterList: []zigbee.ClusterID{0x5678}, 107 | } 108 | 109 | data, err := bytecodec.Marshal(req) 110 | 111 | assert.NoError(t, err) 112 | assert.Equal(t, []byte{0x00, 0x20, 0x01, 0x00, 0x40, 0x0a, 0x08, 0x34, 0x12, 0x78, 0x56, 0x00, 0x01, 0x34, 0x12, 0x01, 0x78, 0x56}, data) 113 | }) 114 | 115 | t.Run("ZdoSimpleDescRsp returns true if success", func(t *testing.T) { 116 | g := ZdoSimpleDescRsp{Status: ZSuccess} 117 | assert.True(t, g.WasSuccessful()) 118 | }) 119 | 120 | t.Run("ZdoSimpleDescRsp returns false if not success", func(t *testing.T) { 121 | g := ZdoSimpleDescRsp{Status: ZFailure} 122 | assert.False(t, g.WasSuccessful()) 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /node_endpoints.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) QueryNodeEndpoints(ctx context.Context, ieeeAddress zigbee.IEEEAddress) ([]zigbee.Endpoint, error) { 10 | networkAddress, err := z.ResolveNodeNWKAddress(ctx, ieeeAddress) 11 | if err != nil { 12 | return []zigbee.Endpoint{}, err 13 | } 14 | 15 | if err := z.sem.Acquire(ctx, 1); err != nil { 16 | return nil, fmt.Errorf("failed to acquire semaphore: %w", err) 17 | } 18 | defer z.sem.Release(1) 19 | 20 | request := ZdoActiveEpReq{ 21 | DestinationAddress: networkAddress, 22 | OfInterestAddress: networkAddress, 23 | } 24 | 25 | resp, err := z.nodeRequest(ctx, &request, &ZdoActiveEpReqReply{}, &ZdoActiveEpRsp{}, func(i interface{}) bool { 26 | msg := i.(*ZdoActiveEpRsp) 27 | return msg.OfInterestAddress == networkAddress 28 | }) 29 | 30 | castResp, ok := resp.(*ZdoActiveEpRsp) 31 | 32 | if ok { 33 | return castResp.ActiveEndpoints, err 34 | } else { 35 | return nil, err 36 | } 37 | } 38 | 39 | type ZdoActiveEpReq struct { 40 | DestinationAddress zigbee.NetworkAddress 41 | OfInterestAddress zigbee.NetworkAddress 42 | } 43 | 44 | const ZdoActiveEpReqID uint8 = 0x05 45 | 46 | type ZdoActiveEpReqReply GenericZStackStatus 47 | 48 | func (r ZdoActiveEpReqReply) WasSuccessful() bool { 49 | return r.Status == ZSuccess 50 | } 51 | 52 | const ZdoActiveEpReqReplyID uint8 = 0x05 53 | 54 | type ZdoActiveEpRsp struct { 55 | SourceAddress zigbee.NetworkAddress 56 | Status ZStackStatus 57 | OfInterestAddress zigbee.NetworkAddress 58 | ActiveEndpoints []zigbee.Endpoint `bcsliceprefix:"8"` 59 | } 60 | 61 | func (r ZdoActiveEpRsp) WasSuccessful() bool { 62 | return r.Status == ZSuccess 63 | } 64 | 65 | const ZdoActiveEpRspID uint8 = 0x85 66 | -------------------------------------------------------------------------------- /node_endpoints_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_QueryNodeEndpoints(t *testing.T) { 17 | t.Run("returns an success on query, response for requested network address is received", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | zstack := New(unpiMock, memory.New()) 23 | zstack.sem = semaphore.NewWeighted(8) 24 | defer unpiMock.Stop() 25 | 26 | unpiMock.On(SREQ, ZDO, ZdoActiveEpReqID).Return(Frame{ 27 | MessageType: SRSP, 28 | Subsystem: ZDO, 29 | CommandID: ZdoActiveEpReqReplyID, 30 | Payload: []byte{0x00}, 31 | }) 32 | 33 | go func() { 34 | time.Sleep(10 * time.Millisecond) 35 | unpiMock.InjectOutgoing(Frame{ 36 | MessageType: AREQ, 37 | Subsystem: ZDO, 38 | CommandID: ZdoActiveEpRspID, 39 | Payload: []byte{0x00, 0x20, 0x00, 0x00, 0x40, 0x03, 0x01, 0x02, 0x03}, 40 | }) 41 | }() 42 | 43 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(0x11223344556677), zigbee.NetworkAddress(0x4000)) 44 | 45 | endpoints, err := zstack.QueryNodeEndpoints(ctx, zigbee.IEEEAddress(0x11223344556677)) 46 | assert.NoError(t, err) 47 | assert.Equal(t, []zigbee.Endpoint{0x01, 0x02, 0x03}, endpoints) 48 | 49 | unpiMock.AssertCalls(t) 50 | }) 51 | } 52 | 53 | func Test_ActiveEndpointMessages(t *testing.T) { 54 | t.Run("verify ZdoActiveEpReq marshals", func(t *testing.T) { 55 | req := ZdoActiveEpReq{ 56 | DestinationAddress: zigbee.NetworkAddress(0x2000), 57 | OfInterestAddress: zigbee.NetworkAddress(0x4000), 58 | } 59 | 60 | data, err := bytecodec.Marshal(req) 61 | 62 | assert.NoError(t, err) 63 | assert.Equal(t, []byte{0x00, 0x20, 0x00, 0x40}, data) 64 | }) 65 | 66 | t.Run("verify ZdoActiveEpReqReply marshals", func(t *testing.T) { 67 | req := ZdoActiveEpReqReply{ 68 | Status: 1, 69 | } 70 | 71 | data, err := bytecodec.Marshal(req) 72 | 73 | assert.NoError(t, err) 74 | assert.Equal(t, []byte{0x01}, data) 75 | }) 76 | 77 | t.Run("generic ZdoActiveEpReqReply returns true if success", func(t *testing.T) { 78 | g := ZdoActiveEpReqReply{Status: ZSuccess} 79 | assert.True(t, g.WasSuccessful()) 80 | }) 81 | 82 | t.Run("generic ZdoActiveEpReqReply returns false if not success", func(t *testing.T) { 83 | g := ZdoActiveEpReqReply{Status: ZFailure} 84 | assert.False(t, g.WasSuccessful()) 85 | }) 86 | 87 | t.Run("verify ZdoActiveEpRsp marshals", func(t *testing.T) { 88 | req := ZdoActiveEpRsp{ 89 | SourceAddress: zigbee.NetworkAddress(0x2000), 90 | Status: 1, 91 | OfInterestAddress: zigbee.NetworkAddress(0x4000), 92 | ActiveEndpoints: []zigbee.Endpoint{0x01, 0x02, 0x03}, 93 | } 94 | 95 | data, err := bytecodec.Marshal(req) 96 | 97 | assert.NoError(t, err) 98 | assert.Equal(t, []byte{0x00, 0x20, 0x01, 0x00, 0x40, 0x03, 0x01, 0x02, 0x03}, data) 99 | }) 100 | 101 | t.Run("generic ZdoActiveEpRsp returns true if success", func(t *testing.T) { 102 | g := ZdoActiveEpRsp{Status: ZSuccess} 103 | assert.True(t, g.WasSuccessful()) 104 | }) 105 | 106 | t.Run("generic ZdoActiveEpRsp returns false if not success", func(t *testing.T) { 107 | g := ZdoActiveEpRsp{Status: ZFailure} 108 | assert.False(t, g.WasSuccessful()) 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /node_receive_message.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/logwrap" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) startMessageReceiver() { 10 | _, z.messageReceiverStop = z.subscriber.Subscribe(&AfIncomingMsg{}, func(v interface{}) { 11 | msg := v.(*AfIncomingMsg) 12 | 13 | ctx, cancel := context.WithTimeout(context.Background(), DefaultResolveIEEETimeout) 14 | defer cancel() 15 | 16 | ieee, err := z.ResolveNodeIEEEAddress(ctx, msg.SourceAddress) 17 | if err != nil { 18 | z.logger.LogError(ctx, "Received AfIncomingMsg (application message), however unable to resolve IEEE address to issue event.", logwrap.Err(err), logwrap.Datum("NetworkAddress", msg.SourceAddress)) 19 | return 20 | } 21 | 22 | node, _ := z.nodeTable.getByIEEE(ieee) 23 | 24 | z.sendEvent(zigbee.NodeIncomingMessageEvent{ 25 | Node: node, 26 | IncomingMessage: zigbee.IncomingMessage{ 27 | GroupID: msg.GroupID, 28 | SourceAddress: zigbee.SourceAddress{ 29 | IEEEAddress: ieee, 30 | NetworkAddress: msg.SourceAddress, 31 | }, 32 | Broadcast: msg.WasBroadcast, 33 | Secure: msg.SecurityUse, 34 | LinkQuality: msg.LinkQuality, 35 | Sequence: msg.Sequence, 36 | ApplicationMessage: zigbee.ApplicationMessage{ 37 | ClusterID: msg.ClusterID, 38 | SourceEndpoint: msg.SourceEndpoint, 39 | DestinationEndpoint: msg.DestinationEndpoint, 40 | Data: msg.Data, 41 | }, 42 | }, 43 | }) 44 | 45 | z.nodeTable.update(ieee, updateReceived(), lqi(msg.LinkQuality)) 46 | }) 47 | } 48 | 49 | func (z *ZStack) stopMessageReceiver() { 50 | if z.messageReceiverStop != nil { 51 | z.messageReceiverStop() 52 | } 53 | } 54 | 55 | type AfIncomingMsg struct { 56 | GroupID zigbee.GroupID 57 | ClusterID zigbee.ClusterID 58 | SourceAddress zigbee.NetworkAddress 59 | SourceEndpoint zigbee.Endpoint 60 | DestinationEndpoint zigbee.Endpoint 61 | WasBroadcast bool 62 | LinkQuality uint8 63 | SecurityUse bool 64 | TimeStamp uint32 65 | Sequence uint8 66 | Data []byte `bcsliceprefix:"8"` 67 | } 68 | 69 | const AfIncomingMsgID uint8 = 0x81 70 | -------------------------------------------------------------------------------- /node_receive_message_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func Test_ReceiveMessage(t *testing.T) { 16 | t.Run("messages which are received with a known network to ieee mapping are sent to event stream", func(t *testing.T) { 17 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 18 | defer cancel() 19 | 20 | unpiMock := unpiTest.NewMockAdapter() 21 | defer unpiMock.AssertCalls(t) 22 | zstack := New(unpiMock, memory.New()) 23 | defer unpiMock.Stop() 24 | 25 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(0x1122334455667788), zigbee.NetworkAddress(0x1000)) 26 | 27 | zstack.startMessageReceiver() 28 | 29 | go func() { 30 | time.Sleep(10 * time.Millisecond) 31 | 32 | msg := AfIncomingMsg{ 33 | GroupID: 0x01, 34 | ClusterID: 0x02, 35 | SourceAddress: 0x1000, 36 | SourceEndpoint: 3, 37 | DestinationEndpoint: 4, 38 | WasBroadcast: true, 39 | LinkQuality: 55, 40 | SecurityUse: true, 41 | TimeStamp: 123412, 42 | Sequence: 63, 43 | Data: []byte{0x01, 0x02}, 44 | } 45 | 46 | data, _ := bytecodec.Marshal(&msg) 47 | 48 | unpiMock.InjectOutgoing(Frame{ 49 | MessageType: AREQ, 50 | Subsystem: AF, 51 | CommandID: AfIncomingMsgID, 52 | Payload: data, 53 | }) 54 | }() 55 | 56 | event, err := zstack.ReadEvent(ctx) 57 | assert.NoError(t, err) 58 | 59 | incommingMsg, ok := event.(zigbee.NodeIncomingMessageEvent) 60 | assert.True(t, ok) 61 | 62 | expectedMsg := zigbee.NodeIncomingMessageEvent{ 63 | Node: zigbee.Node{ 64 | IEEEAddress: 0x1122334455667788, 65 | NetworkAddress: 0x1000, 66 | LogicalType: 0xff, 67 | LQI: 0, 68 | Depth: 0, 69 | LastDiscovered: time.Time{}, 70 | LastReceived: time.Time{}, 71 | }, 72 | IncomingMessage: zigbee.IncomingMessage{ 73 | GroupID: 0x01, 74 | SourceAddress: zigbee.SourceAddress{ 75 | IEEEAddress: 0x1122334455667788, 76 | NetworkAddress: 0x1000, 77 | }, 78 | Broadcast: true, 79 | Secure: true, 80 | LinkQuality: 55, 81 | Sequence: 63, 82 | ApplicationMessage: zigbee.ApplicationMessage{ 83 | ClusterID: 0x02, 84 | SourceEndpoint: 3, 85 | DestinationEndpoint: 4, 86 | Data: []byte{0x01, 0x02}, 87 | }, 88 | }, 89 | } 90 | 91 | assert.Equal(t, expectedMsg, incommingMsg) 92 | 93 | n, _ := zstack.nodeTable.getByNetwork(zigbee.NetworkAddress(0x1000)) 94 | assert.Equal(t, uint8(55), n.LQI) 95 | }) 96 | } 97 | 98 | func Test_IncomingMessage(t *testing.T) { 99 | t.Run("verify AfIncomingMsg marshals", func(t *testing.T) { 100 | req := AfIncomingMsg{ 101 | GroupID: 0x0102, 102 | ClusterID: 0x0304, 103 | SourceAddress: 0x0506, 104 | SourceEndpoint: 0x07, 105 | DestinationEndpoint: 0x08, 106 | WasBroadcast: true, 107 | LinkQuality: 0x0a, 108 | SecurityUse: true, 109 | TimeStamp: 0x10111213, 110 | Sequence: 0x0d, 111 | Data: []byte{0x00, 0x01, 0x02}, 112 | } 113 | 114 | data, err := bytecodec.Marshal(req) 115 | 116 | assert.NoError(t, err) 117 | assert.Equal(t, []byte{0x02, 0x01, 0x04, 0x03, 0x06, 0x05, 0x07, 0x08, 0x01, 0x0a, 0x01, 0x13, 0x12, 0x11, 0x10, 0x0d, 0x03, 0x00, 0x01, 0x02}, data) 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /node_remove.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) RequestNodeLeave(ctx context.Context, nodeAddress zigbee.IEEEAddress) error { 10 | networkAddress, err := z.ResolveNodeNWKAddress(ctx, nodeAddress) 11 | if err != nil { 12 | return nil 13 | } 14 | 15 | if err := z.sem.Acquire(ctx, 1); err != nil { 16 | return fmt.Errorf("failed to acquire semaphore: %w", err) 17 | } 18 | defer z.sem.Release(1) 19 | 20 | request := ZdoMgmtLeaveReq{ 21 | NetworkAddress: networkAddress, 22 | IEEEAddress: nodeAddress, 23 | RemoveChildren: false, 24 | } 25 | 26 | _, err = z.nodeRequest(ctx, &request, &ZdoMgmtLeaveReqReply{}, &ZdoMgmtLeaveRsp{}, func(i interface{}) bool { 27 | msg := i.(*ZdoMgmtLeaveRsp) 28 | return msg.SourceAddress == networkAddress 29 | }) 30 | 31 | return err 32 | } 33 | 34 | func (z *ZStack) ForceNodeLeave(ctx context.Context, nodeAddress zigbee.IEEEAddress) error { 35 | if z.removeNode(nodeAddress) { 36 | return nil 37 | } 38 | 39 | return fmt.Errorf("no node with address provided could be found: %v", nodeAddress) 40 | } 41 | 42 | type ZdoMgmtLeaveReq struct { 43 | NetworkAddress zigbee.NetworkAddress 44 | IEEEAddress zigbee.IEEEAddress 45 | RemoveChildren bool 46 | } 47 | 48 | const ZdoMgmtLeaveReqID uint8 = 0x34 49 | 50 | type ZdoMgmtLeaveReqReply GenericZStackStatus 51 | 52 | func (r ZdoMgmtLeaveReqReply) WasSuccessful() bool { 53 | return r.Status == ZSuccess 54 | } 55 | 56 | const ZdoMgmtLeaveReqReplyID uint8 = 0x34 57 | 58 | type ZdoMgmtLeaveRsp struct { 59 | SourceAddress zigbee.NetworkAddress 60 | Status ZStackStatus 61 | } 62 | 63 | func (r ZdoMgmtLeaveRsp) WasSuccessful() bool { 64 | return r.Status == ZSuccess 65 | } 66 | 67 | const ZdoMgmtLeaveRspID uint8 = 0xb4 68 | -------------------------------------------------------------------------------- /node_remove_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestZStack_RequestNodeLeave(t *testing.T) { 17 | t.Run("returns an success on query, response for requested network address is received", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | zstack := New(unpiMock, memory.New()) 23 | zstack.sem = semaphore.NewWeighted(8) 24 | defer unpiMock.Stop() 25 | 26 | call := unpiMock.On(SREQ, ZDO, ZdoMgmtLeaveReqReplyID).Return(Frame{ 27 | MessageType: SRSP, 28 | Subsystem: ZDO, 29 | CommandID: ZdoMgmtLeaveReqReplyID, 30 | Payload: []byte{0x00}, 31 | }) 32 | 33 | go func() { 34 | time.Sleep(10 * time.Millisecond) 35 | unpiMock.InjectOutgoing(Frame{ 36 | MessageType: AREQ, 37 | Subsystem: ZDO, 38 | CommandID: ZdoMgmtLeaveRspID, 39 | Payload: []byte{0x00, 0x40, 0x00}, 40 | }) 41 | }() 42 | 43 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(1), zigbee.NetworkAddress(0x4000)) 44 | 45 | err := zstack.RequestNodeLeave(ctx, zigbee.IEEEAddress(1)) 46 | assert.NoError(t, err) 47 | 48 | leaveReq := ZdoMgmtLeaveReq{} 49 | bytecodec.Unmarshal(call.CapturedCalls[0].Frame.Payload, &leaveReq) 50 | 51 | assert.Equal(t, zigbee.IEEEAddress(1), leaveReq.IEEEAddress) 52 | assert.Equal(t, zigbee.NetworkAddress(0x4000), leaveReq.NetworkAddress) 53 | assert.False(t, leaveReq.RemoveChildren) 54 | 55 | unpiMock.AssertCalls(t) 56 | }) 57 | } 58 | 59 | func TestZStack_ForceNodeLeave(t *testing.T) { 60 | t.Run("returns success if node was in data", func(t *testing.T) { 61 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 62 | defer cancel() 63 | 64 | unpiMock := unpiTest.NewMockAdapter() 65 | zstack := New(unpiMock, memory.New()) 66 | defer unpiMock.Stop() 67 | 68 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(1), zigbee.NetworkAddress(0x4000)) 69 | 70 | err := zstack.ForceNodeLeave(ctx, zigbee.IEEEAddress(1)) 71 | assert.NoError(t, err) 72 | 73 | unpiMock.AssertCalls(t) 74 | 75 | event, err := zstack.ReadEvent(ctx) 76 | assert.NoError(t, err) 77 | assert.NotNil(t, event) 78 | 79 | nle, ok := event.(zigbee.NodeLeaveEvent) 80 | assert.True(t, ok) 81 | 82 | assert.Equal(t, zigbee.IEEEAddress(1), nle.IEEEAddress) 83 | }) 84 | } 85 | 86 | func Test_RemoveMessages(t *testing.T) { 87 | t.Run("verify ZdoMgmtLeaveReq marshals", func(t *testing.T) { 88 | req := ZdoMgmtLeaveReq{ 89 | NetworkAddress: 0x1234, 90 | IEEEAddress: zigbee.IEEEAddress(0x8899aabbccddeeff), 91 | RemoveChildren: true, 92 | } 93 | 94 | data, err := bytecodec.Marshal(req) 95 | 96 | assert.NoError(t, err) 97 | assert.Equal(t, []byte{0x34, 0x12, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x01}, data) 98 | }) 99 | 100 | t.Run("verify ZdoMgmtLeaveReqReply marshals", func(t *testing.T) { 101 | req := ZdoMgmtLeaveReqReply{ 102 | Status: 1, 103 | } 104 | 105 | data, err := bytecodec.Marshal(req) 106 | 107 | assert.NoError(t, err) 108 | assert.Equal(t, []byte{0x01}, data) 109 | }) 110 | 111 | t.Run("ZdoMgmtLeaveReqReply returns true if success", func(t *testing.T) { 112 | g := ZdoMgmtLeaveReqReply{Status: ZSuccess} 113 | assert.True(t, g.WasSuccessful()) 114 | }) 115 | 116 | t.Run("ZdoMgmtLeaveReqReply returns false if not success", func(t *testing.T) { 117 | g := ZdoMgmtLeaveReqReply{Status: ZFailure} 118 | assert.False(t, g.WasSuccessful()) 119 | }) 120 | 121 | t.Run("verify ZdoMgmtLeaveRsp marshals", func(t *testing.T) { 122 | req := ZdoMgmtLeaveRsp{ 123 | SourceAddress: zigbee.NetworkAddress(0x2000), 124 | Status: 1, 125 | } 126 | 127 | data, err := bytecodec.Marshal(req) 128 | 129 | assert.NoError(t, err) 130 | assert.Equal(t, []byte{0x00, 0x20, 0x01}, data) 131 | }) 132 | 133 | t.Run("ZdoMgmtLeaveRsp returns true if success", func(t *testing.T) { 134 | g := ZdoMgmtLeaveRsp{Status: ZSuccess} 135 | assert.True(t, g.WasSuccessful()) 136 | }) 137 | 138 | t.Run("ZdoMgmtLeaveRsp returns false if not success", func(t *testing.T) { 139 | g := ZdoMgmtLeaveRsp{Status: ZFailure} 140 | assert.False(t, g.WasSuccessful()) 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /node_request.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | func AnyResponse(v interface{}) bool { 9 | return true 10 | } 11 | 12 | var ReplyDoesNotReportSuccess = errors.New("reply struct does not support Successor interface") 13 | var NodeResponseWasNotSuccess = errors.New("response from node was not success") 14 | 15 | func (z *ZStack) nodeRequest(ctx context.Context, request interface{}, reply interface{}, response interface{}, responseFilter func(interface{}) bool) (interface{}, error) { 16 | replySuccessor, replySupportsSuccessor := reply.(Successor) 17 | 18 | if !replySupportsSuccessor { 19 | return nil, ReplyDoesNotReportSuccess 20 | } 21 | 22 | ch := make(chan interface{}) 23 | 24 | err, stop := z.subscriber.Subscribe(response, func(v interface{}) { 25 | if responseFilter(v) { 26 | select { 27 | case ch <- v: 28 | case <-ctx.Done(): 29 | } 30 | } 31 | }) 32 | defer stop() 33 | 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | if err := z.requestResponder.RequestResponse(ctx, request, reply); err != nil { 39 | return nil, err 40 | } 41 | 42 | if !replySuccessor.WasSuccessful() { 43 | return nil, ErrorZFailure 44 | } 45 | 46 | select { 47 | case v := <-ch: 48 | responseSuccessor, responseSupportsSuccessor := v.(Successor) 49 | 50 | if responseSupportsSuccessor && !responseSuccessor.WasSuccessful() { 51 | return v, NodeResponseWasNotSuccess 52 | } 53 | return v, nil 54 | case <-ctx.Done(): 55 | return nil, errors.New("context expired while waiting for response from node") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /node_request_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/persistence/impl/memory" 6 | . "github.com/shimmeringbee/unpi" 7 | unpiTest "github.com/shimmeringbee/unpi/testing" 8 | "github.com/shimmeringbee/zigbee" 9 | "github.com/stretchr/testify/assert" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func Test_NodeRequest(t *testing.T) { 15 | t.Run("returns an error if the response type does not support Successor", func(t *testing.T) { 16 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 17 | defer cancel() 18 | 19 | unpiMock := unpiTest.NewMockAdapter() 20 | zstack := New(unpiMock, memory.New()) 21 | defer unpiMock.Stop() 22 | 23 | type NotSuccessful struct{} 24 | 25 | resp, err := zstack.nodeRequest(ctx, &NotSuccessful{}, &NotSuccessful{}, &NotSuccessful{}, AnyResponse) 26 | 27 | assert.Error(t, err) 28 | assert.Nil(t, resp) 29 | assert.Equal(t, ReplyDoesNotReportSuccess, err) 30 | }) 31 | 32 | t.Run("returns an error if the request results in a failed response", func(t *testing.T) { 33 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 34 | defer cancel() 35 | 36 | unpiMock := unpiTest.NewMockAdapter() 37 | zstack := New(unpiMock, memory.New()) 38 | defer unpiMock.Stop() 39 | 40 | unpiMock.On(SREQ, ZDO, ZdoActiveEpReqID).Return(Frame{ 41 | MessageType: SRSP, 42 | Subsystem: ZDO, 43 | CommandID: ZdoActiveEpReqReplyID, 44 | Payload: []byte{0x01}, 45 | }) 46 | 47 | resp, err := zstack.nodeRequest(ctx, &ZdoActiveEpReq{}, &ZdoActiveEpReqReply{}, &ZdoActiveEpRsp{}, AnyResponse) 48 | 49 | assert.Error(t, err) 50 | assert.Nil(t, resp) 51 | assert.Equal(t, ErrorZFailure, err) 52 | 53 | unpiMock.AssertCalls(t) 54 | }) 55 | 56 | t.Run("returns a success, when request was successfully replied to, and response has been received", func(t *testing.T) { 57 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 58 | defer cancel() 59 | 60 | unpiMock := unpiTest.NewMockAdapter() 61 | zstack := New(unpiMock, memory.New()) 62 | defer unpiMock.Stop() 63 | 64 | unpiMock.On(SREQ, ZDO, ZdoActiveEpReqID).Return(Frame{ 65 | MessageType: SRSP, 66 | Subsystem: ZDO, 67 | CommandID: ZdoActiveEpReqReplyID, 68 | Payload: []byte{0x00}, 69 | }) 70 | 71 | go func() { 72 | time.Sleep(10 * time.Millisecond) 73 | unpiMock.InjectOutgoing(Frame{ 74 | MessageType: AREQ, 75 | Subsystem: ZDO, 76 | CommandID: ZdoActiveEpRspID, 77 | Payload: []byte{0x00, 0x20, 0x00, 0x00, 0x40, 0x03, 0x01, 0x02, 0x03}, 78 | }) 79 | }() 80 | 81 | resp, err := zstack.nodeRequest(ctx, &ZdoActiveEpReq{DestinationAddress: 0x4000, OfInterestAddress: 0x4000}, &ZdoActiveEpReqReply{}, &ZdoActiveEpRsp{}, AnyResponse) 82 | castResp, ok := resp.(*ZdoActiveEpRsp) 83 | 84 | assert.NoError(t, err) 85 | assert.True(t, ok) 86 | assert.Equal(t, []zigbee.Endpoint{0x01, 0x02, 0x03}, castResp.ActiveEndpoints) 87 | 88 | unpiMock.AssertCalls(t) 89 | }) 90 | 91 | t.Run("returns a success, when request was successfully replied to with a response which is unwanted, then wanted", func(t *testing.T) { 92 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 93 | defer cancel() 94 | 95 | unpiMock := unpiTest.NewMockAdapter() 96 | zstack := New(unpiMock, memory.New()) 97 | defer unpiMock.Stop() 98 | 99 | unpiMock.On(SREQ, ZDO, ZdoActiveEpReqID).Return(Frame{ 100 | MessageType: SRSP, 101 | Subsystem: ZDO, 102 | CommandID: ZdoActiveEpReqReplyID, 103 | Payload: []byte{0x00}, 104 | }) 105 | 106 | go func() { 107 | time.Sleep(10 * time.Millisecond) 108 | unpiMock.InjectOutgoing(Frame{ 109 | MessageType: AREQ, 110 | Subsystem: ZDO, 111 | CommandID: ZdoActiveEpRspID, 112 | Payload: []byte{0x00, 0x20, 0x00, 0x00, 0x20, 0x03, 0x01, 0x02, 0x03}, 113 | }) 114 | time.Sleep(10 * time.Millisecond) 115 | unpiMock.InjectOutgoing(Frame{ 116 | MessageType: AREQ, 117 | Subsystem: ZDO, 118 | CommandID: ZdoActiveEpRspID, 119 | Payload: []byte{0x00, 0x20, 0x00, 0x00, 0x40, 0x01, 0x02}, 120 | }) 121 | }() 122 | 123 | resp, err := zstack.nodeRequest(ctx, &ZdoActiveEpReq{DestinationAddress: 0x4000, OfInterestAddress: 0x4000}, &ZdoActiveEpReqReply{}, &ZdoActiveEpRsp{}, func(i interface{}) bool { 124 | response := i.(*ZdoActiveEpRsp) 125 | return response.OfInterestAddress == 0x4000 126 | }) 127 | castResp, ok := resp.(*ZdoActiveEpRsp) 128 | 129 | assert.NoError(t, err) 130 | assert.True(t, ok) 131 | assert.Equal(t, []zigbee.Endpoint{0x02}, castResp.ActiveEndpoints) 132 | 133 | unpiMock.AssertCalls(t) 134 | }) 135 | 136 | t.Run("returns a success, when request was successfully replied to with a response which is wanted, then unwanted", func(t *testing.T) { 137 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 138 | defer cancel() 139 | 140 | unpiMock := unpiTest.NewMockAdapter() 141 | zstack := New(unpiMock, memory.New()) 142 | defer unpiMock.Stop() 143 | 144 | unpiMock.On(SREQ, ZDO, ZdoActiveEpReqID).Return(Frame{ 145 | MessageType: SRSP, 146 | Subsystem: ZDO, 147 | CommandID: ZdoActiveEpReqReplyID, 148 | Payload: []byte{0x00}, 149 | }) 150 | 151 | go func() { 152 | time.Sleep(10 * time.Millisecond) 153 | unpiMock.InjectOutgoing(Frame{ 154 | MessageType: AREQ, 155 | Subsystem: ZDO, 156 | CommandID: ZdoActiveEpRspID, 157 | Payload: []byte{0x00, 0x20, 0x00, 0x00, 0x40, 0x01, 0x02}, 158 | }) 159 | unpiMock.InjectOutgoing(Frame{ 160 | MessageType: AREQ, 161 | Subsystem: ZDO, 162 | CommandID: ZdoActiveEpRspID, 163 | Payload: []byte{0x00, 0x20, 0x00, 0x00, 0x20, 0x03, 0x01, 0x02, 0x03}, 164 | }) 165 | }() 166 | 167 | resp, err := zstack.nodeRequest(ctx, &ZdoActiveEpReq{DestinationAddress: 0x4000, OfInterestAddress: 0x4000}, &ZdoActiveEpReqReply{}, &ZdoActiveEpRsp{}, func(i interface{}) bool { 168 | return i.(*ZdoActiveEpRsp).OfInterestAddress == 0x4000 169 | }) 170 | castResp, ok := resp.(*ZdoActiveEpRsp) 171 | 172 | assert.NoError(t, err) 173 | assert.True(t, ok) 174 | assert.Equal(t, []zigbee.Endpoint{0x02}, castResp.ActiveEndpoints) 175 | 176 | unpiMock.AssertCalls(t) 177 | }) 178 | 179 | t.Run("returns an error, when request was successfully replied to, but response supports WasSuccessful and is a failure", func(t *testing.T) { 180 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 181 | defer cancel() 182 | 183 | unpiMock := unpiTest.NewMockAdapter() 184 | zstack := New(unpiMock, memory.New()) 185 | defer unpiMock.Stop() 186 | 187 | unpiMock.On(SREQ, ZDO, ZdoActiveEpReqID).Return(Frame{ 188 | MessageType: SRSP, 189 | Subsystem: ZDO, 190 | CommandID: ZdoActiveEpReqReplyID, 191 | Payload: []byte{0x00}, 192 | }) 193 | 194 | go func() { 195 | time.Sleep(10 * time.Millisecond) 196 | unpiMock.InjectOutgoing(Frame{ 197 | MessageType: AREQ, 198 | Subsystem: ZDO, 199 | CommandID: ZdoActiveEpRspID, 200 | Payload: []byte{0x00, 0x20, 0x01, 0x00, 0x40, 0x03, 0x01, 0x02, 0x03}, 201 | }) 202 | }() 203 | 204 | resp, err := zstack.nodeRequest(ctx, &ZdoActiveEpReq{DestinationAddress: 0x4000, OfInterestAddress: 0x4000}, &ZdoActiveEpReqReply{}, &ZdoActiveEpRsp{}, AnyResponse) 205 | castResp, ok := resp.(*ZdoActiveEpRsp) 206 | 207 | assert.Error(t, err) 208 | assert.True(t, ok) 209 | assert.Equal(t, NodeResponseWasNotSuccess, err) 210 | assert.Equal(t, []zigbee.Endpoint{0x01, 0x02, 0x03}, castResp.ActiveEndpoints) 211 | 212 | unpiMock.AssertCalls(t) 213 | }) 214 | } 215 | -------------------------------------------------------------------------------- /node_send_message.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/shimmeringbee/logwrap" 8 | "github.com/shimmeringbee/zigbee" 9 | ) 10 | 11 | const DefaultRadius uint8 = 0x20 12 | 13 | func (z *ZStack) SendApplicationMessageToNode(ctx context.Context, destinationAddress zigbee.IEEEAddress, message zigbee.ApplicationMessage, requireAck bool) error { 14 | network, err := z.ResolveNodeNWKAddress(ctx, destinationAddress) 15 | if err != nil { 16 | z.logger.LogError(ctx, "Failed to send AfDataRequest (application message), failed to resolve IEEE Address to Network Adddress.", logwrap.Err(err), logwrap.Datum("IEEEAddress", destinationAddress.String())) 17 | return err 18 | } 19 | 20 | if err := z.sem.Acquire(ctx, 1); err != nil { 21 | return fmt.Errorf("failed to acquire semaphore: %w", err) 22 | } 23 | defer z.sem.Release(1) 24 | 25 | var transactionId uint8 26 | 27 | select { 28 | case transactionId = <-z.transactionIdStore: 29 | defer func() { z.transactionIdStore <- transactionId }() 30 | case <-ctx.Done(): 31 | return errors.New("context expired while obtaining a free transaction ID") 32 | } 33 | 34 | request := AfDataRequest{ 35 | DestinationAddress: network, 36 | DestinationEndpoint: message.DestinationEndpoint, 37 | SourceEndpoint: message.SourceEndpoint, 38 | ClusterID: message.ClusterID, 39 | TransactionID: transactionId, 40 | Options: AfDataRequestOptions{ACKRequest: requireAck}, 41 | Radius: DefaultRadius, 42 | Data: message.Data, 43 | } 44 | 45 | if requireAck { 46 | _, err = z.nodeRequest(ctx, &request, &AfDataRequestReply{}, &AfDataConfirm{}, func(i interface{}) bool { 47 | msg := i.(*AfDataConfirm) 48 | return msg.TransactionID == transactionId && msg.Endpoint == message.DestinationEndpoint 49 | }) 50 | } else { 51 | err = z.requestResponder.RequestResponse(ctx, &request, &AfDataRequestReply{}) 52 | } 53 | 54 | return err 55 | } 56 | 57 | type AfDataRequestOptions struct { 58 | Reserved0 uint8 `bcfieldwidth:"1"` 59 | EnableSecurity bool `bcfieldwidth:"1"` 60 | DiscoveryRoute bool `bcfieldwidth:"1"` 61 | ACKRequest bool `bcfieldwidth:"1"` 62 | Reserved1 uint8 `bcfieldwidth:"4"` 63 | } 64 | 65 | type AfDataRequest struct { 66 | DestinationAddress zigbee.NetworkAddress 67 | DestinationEndpoint zigbee.Endpoint 68 | SourceEndpoint zigbee.Endpoint 69 | ClusterID zigbee.ClusterID 70 | TransactionID uint8 71 | Options AfDataRequestOptions 72 | Radius uint8 73 | Data []byte `bcsliceprefix:"8"` 74 | } 75 | 76 | const AfDataRequestID uint8 = 0x01 77 | 78 | type AfDataRequestReply GenericZStackStatus 79 | 80 | func (s AfDataRequestReply) WasSuccessful() bool { 81 | return s.Status == ZSuccess 82 | } 83 | 84 | const AfDataRequestReplyID uint8 = 0x01 85 | 86 | type AfDataConfirm struct { 87 | Status ZStackStatus 88 | Endpoint zigbee.Endpoint 89 | TransactionID uint8 90 | } 91 | 92 | func (s AfDataConfirm) WasSuccessful() bool { 93 | return s.Status == ZSuccess 94 | } 95 | 96 | const AfDataConfirmID uint8 = 0x80 97 | -------------------------------------------------------------------------------- /node_send_message_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_SendNodeMessage(t *testing.T) { 17 | t.Run("messages with ack wait", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | defer unpiMock.AssertCalls(t) 23 | zstack := New(unpiMock, memory.New()) 24 | zstack.sem = semaphore.NewWeighted(8) 25 | defer unpiMock.Stop() 26 | 27 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(0x1122334455667788), zigbee.NetworkAddress(0x1000)) 28 | 29 | c := unpiMock.On(SREQ, AF, AfDataRequestID).Return(Frame{ 30 | MessageType: SRSP, 31 | Subsystem: AF, 32 | CommandID: AfDataRequestReplyID, 33 | Payload: []byte{0x00}, 34 | }) 35 | 36 | go func() { 37 | time.Sleep(10 * time.Millisecond) 38 | 39 | unpiMock.InjectOutgoing(Frame{ 40 | MessageType: AREQ, 41 | Subsystem: AF, 42 | CommandID: AfDataConfirmID, 43 | Payload: []byte{0x00, 0x04, 0x00}, 44 | }) 45 | }() 46 | 47 | appMessage := zigbee.ApplicationMessage{ 48 | ClusterID: 0x2000, 49 | SourceEndpoint: 0x03, 50 | DestinationEndpoint: 0x04, 51 | Data: []byte{0x0a, 0x0b}, 52 | } 53 | 54 | err := zstack.SendApplicationMessageToNode(ctx, zigbee.IEEEAddress(0x1122334455667788), appMessage, true) 55 | assert.NoError(t, err) 56 | 57 | sentFrame := c.CapturedCalls[0].Frame 58 | 59 | assert.Equal(t, []byte{0x00, 0x10, 0x04, 0x03, 0x00, 0x20, 0x00, 0x10, 0x20, 0x02, 0x0a, 0x0b}, sentFrame.Payload) 60 | }) 61 | 62 | t.Run("messages without ack just return", func(t *testing.T) { 63 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 64 | defer cancel() 65 | 66 | unpiMock := unpiTest.NewMockAdapter() 67 | defer unpiMock.AssertCalls(t) 68 | zstack := New(unpiMock, memory.New()) 69 | zstack.sem = semaphore.NewWeighted(8) 70 | defer unpiMock.Stop() 71 | 72 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(0x1122334455667788), zigbee.NetworkAddress(0x1000)) 73 | 74 | c := unpiMock.On(SREQ, AF, AfDataRequestID).Return(Frame{ 75 | MessageType: SRSP, 76 | Subsystem: AF, 77 | CommandID: AfDataRequestReplyID, 78 | Payload: []byte{0x00}, 79 | }) 80 | 81 | appMessage := zigbee.ApplicationMessage{ 82 | ClusterID: 0x2000, 83 | SourceEndpoint: 0x03, 84 | DestinationEndpoint: 0x04, 85 | Data: []byte{0x0a, 0x0b}, 86 | } 87 | 88 | err := zstack.SendApplicationMessageToNode(ctx, zigbee.IEEEAddress(0x1122334455667788), appMessage, false) 89 | assert.NoError(t, err) 90 | 91 | sentFrame := c.CapturedCalls[0].Frame 92 | 93 | assert.Equal(t, []byte{0x00, 0x10, 0x04, 0x03, 0x00, 0x20, 0x00, 0x0, 0x20, 0x02, 0x0a, 0x0b}, sentFrame.Payload) 94 | }) 95 | } 96 | 97 | func Test_SendMessages(t *testing.T) { 98 | t.Run("verify AfDataRequest marshals", func(t *testing.T) { 99 | req := AfDataRequest{ 100 | DestinationAddress: 0x0102, 101 | DestinationEndpoint: 0x03, 102 | SourceEndpoint: 0x04, 103 | ClusterID: 0x0506, 104 | TransactionID: 0x07, 105 | Options: AfDataRequestOptions{ 106 | EnableSecurity: true, 107 | DiscoveryRoute: true, 108 | ACKRequest: true, 109 | }, 110 | Radius: 0x09, 111 | Data: []byte{0x0a, 0x0b}, 112 | } 113 | 114 | data, err := bytecodec.Marshal(req) 115 | 116 | assert.NoError(t, err) 117 | assert.Equal(t, []byte{0x02, 0x01, 0x03, 0x04, 0x06, 0x05, 0x07, 0x70, 0x09, 0x02, 0x0a, 0x0b}, data) 118 | }) 119 | 120 | t.Run("verify AfDataRequestReply marshals", func(t *testing.T) { 121 | req := AfDataRequestReply{ 122 | Status: 1, 123 | } 124 | 125 | data, err := bytecodec.Marshal(req) 126 | 127 | assert.NoError(t, err) 128 | assert.Equal(t, []byte{0x01}, data) 129 | }) 130 | 131 | t.Run("AfDataRequestReply returns true if success", func(t *testing.T) { 132 | g := AfDataRequestReply{Status: ZSuccess} 133 | assert.True(t, g.WasSuccessful()) 134 | }) 135 | 136 | t.Run("AfDataRequestReply returns false if not success", func(t *testing.T) { 137 | g := AfDataRequestReply{Status: ZFailure} 138 | assert.False(t, g.WasSuccessful()) 139 | }) 140 | 141 | t.Run("verify AfDataConfirm marshals", func(t *testing.T) { 142 | req := AfDataConfirm{ 143 | Status: 0x01, 144 | Endpoint: 0x02, 145 | TransactionID: 0x03, 146 | } 147 | 148 | data, err := bytecodec.Marshal(req) 149 | 150 | assert.NoError(t, err) 151 | assert.Equal(t, []byte{0x01, 0x02, 0x03}, data) 152 | }) 153 | 154 | t.Run("AfDataConfirm returns true if success", func(t *testing.T) { 155 | g := AfDataConfirm{Status: ZSuccess} 156 | assert.True(t, g.WasSuccessful()) 157 | }) 158 | 159 | t.Run("AfDataConfirm returns false if not success", func(t *testing.T) { 160 | g := AfDataConfirm{Status: ZFailure} 161 | assert.False(t, g.WasSuccessful()) 162 | }) 163 | } 164 | -------------------------------------------------------------------------------- /node_table.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "github.com/shimmeringbee/persistence" 5 | "github.com/shimmeringbee/persistence/converter" 6 | "github.com/shimmeringbee/zigbee" 7 | "strconv" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type nodeTable struct { 13 | callbacks []func(zigbee.Node) 14 | ieeeToNode map[zigbee.IEEEAddress]*zigbee.Node 15 | networkToIEEE map[zigbee.NetworkAddress]zigbee.IEEEAddress 16 | lock *sync.RWMutex 17 | 18 | p persistence.Section 19 | loading bool 20 | } 21 | 22 | func newNodeTable(p persistence.Section) *nodeTable { 23 | n := &nodeTable{ 24 | callbacks: []func(zigbee.Node){}, 25 | ieeeToNode: make(map[zigbee.IEEEAddress]*zigbee.Node), 26 | networkToIEEE: make(map[zigbee.NetworkAddress]zigbee.IEEEAddress), 27 | lock: &sync.RWMutex{}, 28 | p: p, 29 | } 30 | 31 | n.load() 32 | 33 | return n 34 | } 35 | 36 | func (t *nodeTable) registerCallback(cb func(zigbee.Node)) { 37 | t.callbacks = append(t.callbacks, cb) 38 | } 39 | 40 | func (t *nodeTable) nodes() []zigbee.Node { 41 | t.lock.RLock() 42 | defer t.lock.RUnlock() 43 | 44 | var nodes []zigbee.Node 45 | 46 | for _, node := range t.ieeeToNode { 47 | nodes = append(nodes, *node) 48 | } 49 | 50 | return nodes 51 | } 52 | 53 | func (t *nodeTable) getByIEEE(ieeeAddress zigbee.IEEEAddress) (zigbee.Node, bool) { 54 | t.lock.RLock() 55 | defer t.lock.RUnlock() 56 | 57 | node, found := t.ieeeToNode[ieeeAddress] 58 | 59 | if found { 60 | return *node, found 61 | } else { 62 | return zigbee.Node{}, false 63 | } 64 | } 65 | 66 | func (t *nodeTable) getByNetwork(networkAddress zigbee.NetworkAddress) (zigbee.Node, bool) { 67 | t.lock.RLock() 68 | ieee, found := t.networkToIEEE[networkAddress] 69 | t.lock.RUnlock() 70 | 71 | if !found { 72 | return zigbee.Node{}, false 73 | } else { 74 | return t.getByIEEE(ieee) 75 | } 76 | } 77 | 78 | func (t *nodeTable) addOrUpdate(ieeeAddress zigbee.IEEEAddress, networkAddress zigbee.NetworkAddress, updates ...nodeUpdate) { 79 | t.lock.Lock() 80 | node, found := t.ieeeToNode[ieeeAddress] 81 | 82 | s := t.p.Section(ieeeAddress.String()) 83 | 84 | if found { 85 | if node.NetworkAddress != networkAddress { 86 | delete(t.networkToIEEE, node.NetworkAddress) 87 | node.NetworkAddress = networkAddress 88 | 89 | if !t.loading { 90 | converter.Store(s, "NetworkAddress", node.NetworkAddress, converter.NetworkAddressEncoder) 91 | } 92 | } 93 | } else { 94 | node = &zigbee.Node{ 95 | IEEEAddress: ieeeAddress, 96 | NetworkAddress: networkAddress, 97 | LogicalType: zigbee.Unknown, 98 | } 99 | 100 | t.ieeeToNode[ieeeAddress] = node 101 | 102 | if !t.loading { 103 | converter.Store(s, "NetworkAddress", node.NetworkAddress, converter.NetworkAddressEncoder) 104 | converter.Store(s, "LogicalType", node.LogicalType, converter.LogicalTypeEncoder) 105 | } 106 | } 107 | 108 | t.networkToIEEE[networkAddress] = ieeeAddress 109 | t.lock.Unlock() 110 | 111 | t.update(ieeeAddress, updates...) 112 | } 113 | 114 | func (t *nodeTable) update(ieeeAddress zigbee.IEEEAddress, updates ...nodeUpdate) { 115 | t.lock.Lock() 116 | defer t.lock.Unlock() 117 | 118 | node, found := t.ieeeToNode[ieeeAddress] 119 | 120 | if found { 121 | var s persistence.Section 122 | 123 | if !t.loading { 124 | s = t.p.Section(ieeeAddress.String()) 125 | } 126 | 127 | for _, du := range updates { 128 | du(node, s) 129 | } 130 | 131 | for _, cb := range t.callbacks { 132 | cb(*node) 133 | } 134 | } 135 | } 136 | 137 | func (t *nodeTable) remove(ieeeAddress zigbee.IEEEAddress) { 138 | node, found := t.getByIEEE(ieeeAddress) 139 | 140 | t.lock.Lock() 141 | defer t.lock.Unlock() 142 | 143 | if found { 144 | delete(t.networkToIEEE, node.NetworkAddress) 145 | delete(t.ieeeToNode, node.IEEEAddress) 146 | } 147 | 148 | t.p.SectionDelete(ieeeAddress.String()) 149 | } 150 | 151 | func (t *nodeTable) load() { 152 | t.lock.Lock() 153 | t.loading = true 154 | t.lock.Unlock() 155 | 156 | defer func() { 157 | t.lock.Lock() 158 | t.loading = false 159 | t.lock.Unlock() 160 | }() 161 | 162 | for _, key := range t.p.SectionKeys() { 163 | if value, err := strconv.ParseUint(key, 16, 64); err == nil { 164 | s := t.p.Section(key) 165 | ieee := zigbee.IEEEAddress(value) 166 | 167 | na, naFound := converter.Retrieve(s, "NetworkAddress", converter.NetworkAddressDecoder) 168 | if !naFound { 169 | continue 170 | } 171 | 172 | t.addOrUpdate(ieee, na) 173 | 174 | if lt, found := converter.Retrieve(s, "LogicalType", converter.LogicalTypeDecoder); found { 175 | t.update(ieee, logicalType(lt)) 176 | } 177 | 178 | if l, found := s.UInt("LQI"); found { 179 | t.update(ieee, lqi(uint8(l))) 180 | } 181 | 182 | if d, found := s.UInt("Depth"); found { 183 | t.update(ieee, depth(uint8(d))) 184 | } 185 | 186 | if received, found := converter.Retrieve(s, "LastReceived", converter.TimeDecoder); found { 187 | t.update(ieee, setReceived(received)) 188 | 189 | } 190 | 191 | if discovered, found := converter.Retrieve(s, "LastDiscovered", converter.TimeDecoder); found { 192 | t.update(ieee, setDiscovered(discovered)) 193 | } 194 | } 195 | } 196 | } 197 | 198 | type nodeUpdate func(node *zigbee.Node, p persistence.Section) 199 | 200 | func logicalType(logicalType zigbee.LogicalType) nodeUpdate { 201 | return func(node *zigbee.Node, p persistence.Section) { 202 | node.LogicalType = logicalType 203 | 204 | if p != nil { 205 | converter.Store(p, "LogicalType", node.LogicalType, converter.LogicalTypeEncoder) 206 | } 207 | } 208 | } 209 | 210 | func lqi(lqi uint8) nodeUpdate { 211 | return func(node *zigbee.Node, p persistence.Section) { 212 | node.LQI = lqi 213 | 214 | if p != nil { 215 | p.Set("LQI", uint64(node.LQI)) 216 | } 217 | } 218 | } 219 | 220 | func depth(depth uint8) nodeUpdate { 221 | return func(node *zigbee.Node, p persistence.Section) { 222 | node.Depth = depth 223 | 224 | if p != nil { 225 | p.Set("Depth", uint64(node.Depth)) 226 | } 227 | } 228 | } 229 | 230 | func updateReceived() nodeUpdate { 231 | return setReceived(time.Now()) 232 | } 233 | 234 | func updateDiscovered() nodeUpdate { 235 | return setDiscovered(time.Now()) 236 | } 237 | 238 | func setReceived(t time.Time) nodeUpdate { 239 | return func(node *zigbee.Node, p persistence.Section) { 240 | node.LastReceived = t 241 | 242 | if p != nil { 243 | converter.Store(p, "LastReceived", node.LastReceived, converter.TimeEncoder) 244 | } 245 | } 246 | } 247 | 248 | func setDiscovered(t time.Time) nodeUpdate { 249 | return func(node *zigbee.Node, p persistence.Section) { 250 | node.LastDiscovered = t 251 | 252 | if p != nil { 253 | converter.Store(p, "LastDiscovered", node.LastDiscovered, converter.TimeEncoder) 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /node_table_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "github.com/shimmeringbee/persistence/converter" 5 | "github.com/shimmeringbee/persistence/impl/memory" 6 | "github.com/shimmeringbee/zigbee" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestNodeTable(t *testing.T) { 13 | ieee := zigbee.GenerateLocalAdministeredIEEEAddress() 14 | network := zigbee.NetworkAddress(0xaabb) 15 | 16 | t.Run("an added node can be retrieved by its IEEE address, and has minimum information", func(t *testing.T) { 17 | s := memory.New() 18 | nt := newNodeTable(s) 19 | 20 | nt.addOrUpdate(ieee, network) 21 | 22 | node, found := nt.getByIEEE(ieee) 23 | 24 | assert.True(t, found) 25 | assert.Equal(t, ieee, node.IEEEAddress) 26 | assert.Equal(t, network, node.NetworkAddress) 27 | assert.Equal(t, zigbee.Unknown, node.LogicalType) 28 | 29 | assert.Contains(t, s.SectionKeys(), ieee.String()) 30 | 31 | ns := s.Section(ieee.String()) 32 | na, ok := converter.Retrieve(ns, "NetworkAddress", converter.NetworkAddressDecoder) 33 | 34 | assert.True(t, ok) 35 | assert.Equal(t, network, na) 36 | }) 37 | 38 | t.Run("an added node with updates can be retrieved and has updated information", func(t *testing.T) { 39 | s := memory.New() 40 | nt := newNodeTable(s) 41 | 42 | nt.addOrUpdate(ieee, network, logicalType(zigbee.EndDevice)) 43 | 44 | node, found := nt.getByIEEE(ieee) 45 | 46 | assert.True(t, found) 47 | assert.Equal(t, zigbee.EndDevice, node.LogicalType) 48 | 49 | ns := s.Section(ieee.String()) 50 | lt, ok := converter.Retrieve(ns, "LogicalType", converter.LogicalTypeDecoder) 51 | 52 | assert.True(t, ok) 53 | assert.Equal(t, zigbee.EndDevice, lt) 54 | }) 55 | 56 | t.Run("an added node can be retrieved by its network address", func(t *testing.T) { 57 | nt := newNodeTable(memory.New()) 58 | 59 | nt.addOrUpdate(ieee, network) 60 | 61 | _, found := nt.getByNetwork(network) 62 | assert.True(t, found) 63 | }) 64 | 65 | t.Run("a missing node queried by its ieee address returns not found", func(t *testing.T) { 66 | nt := newNodeTable(memory.New()) 67 | 68 | _, found := nt.getByIEEE(ieee) 69 | assert.False(t, found) 70 | }) 71 | 72 | t.Run("a missing node queried by its network address returns not found", func(t *testing.T) { 73 | nt := newNodeTable(memory.New()) 74 | 75 | _, found := nt.getByNetwork(network) 76 | assert.False(t, found) 77 | }) 78 | 79 | t.Run("removing a node results in it not being found by ieee address", func(t *testing.T) { 80 | nt := newNodeTable(memory.New()) 81 | 82 | nt.addOrUpdate(ieee, network) 83 | nt.remove(ieee) 84 | 85 | _, found := nt.getByIEEE(ieee) 86 | assert.False(t, found) 87 | }) 88 | 89 | t.Run("removing a node results in it not being found by network address", func(t *testing.T) { 90 | s := memory.New() 91 | nt := newNodeTable(s) 92 | 93 | nt.addOrUpdate(ieee, network) 94 | assert.Contains(t, s.SectionKeys(), ieee.String()) 95 | 96 | nt.remove(ieee) 97 | assert.NotContains(t, s.SectionKeys(), ieee.String()) 98 | 99 | _, found := nt.getByNetwork(network) 100 | assert.False(t, found) 101 | }) 102 | 103 | t.Run("an update using add makes the node available under the new network only, and updates the network address", func(t *testing.T) { 104 | s := memory.New() 105 | nt := newNodeTable(s) 106 | 107 | newNetwork := zigbee.NetworkAddress(0x1234) 108 | 109 | nt.addOrUpdate(ieee, network) 110 | nt.addOrUpdate(ieee, newNetwork) 111 | 112 | _, found := nt.getByNetwork(network) 113 | assert.False(t, found) 114 | 115 | node, found := nt.getByNetwork(newNetwork) 116 | assert.True(t, found) 117 | 118 | assert.Equal(t, newNetwork, node.NetworkAddress) 119 | 120 | ns := s.Section(ieee.String()) 121 | na, ok := converter.Retrieve(ns, "NetworkAddress", converter.NetworkAddressDecoder) 122 | 123 | assert.True(t, ok) 124 | assert.Equal(t, newNetwork, na) 125 | }) 126 | 127 | t.Run("an update makes all changes as requested by node updates", func(t *testing.T) { 128 | nt := newNodeTable(memory.New()) 129 | 130 | nt.addOrUpdate(ieee, network) 131 | 132 | nt.update(ieee, logicalType(zigbee.EndDevice)) 133 | 134 | d, _ := nt.getByIEEE(ieee) 135 | 136 | assert.Equal(t, zigbee.EndDevice, d.LogicalType) 137 | }) 138 | 139 | t.Run("returns all nodes when queried", func(t *testing.T) { 140 | nt := newNodeTable(memory.New()) 141 | 142 | nt.addOrUpdate(ieee, network) 143 | 144 | nodes := nt.nodes() 145 | assert.Equal(t, 1, len(nodes)) 146 | }) 147 | 148 | t.Run("callbacks are called for additions", func(t *testing.T) { 149 | callbackCalled := false 150 | 151 | nt := newNodeTable(memory.New()) 152 | nt.registerCallback(func(node zigbee.Node) { 153 | callbackCalled = true 154 | }) 155 | 156 | nt.addOrUpdate(zigbee.IEEEAddress(0x00), zigbee.NetworkAddress(0x00)) 157 | 158 | assert.True(t, callbackCalled) 159 | }) 160 | 161 | t.Run("callbacks are called for additions", func(t *testing.T) { 162 | callbackCalled := false 163 | 164 | nt := newNodeTable(memory.New()) 165 | 166 | nt.addOrUpdate(zigbee.IEEEAddress(0x00), zigbee.NetworkAddress(0x00)) 167 | 168 | nt.registerCallback(func(node zigbee.Node) { 169 | callbackCalled = true 170 | }) 171 | 172 | nt.update(zigbee.IEEEAddress(0x00), updateReceived()) 173 | 174 | assert.True(t, callbackCalled) 175 | }) 176 | } 177 | 178 | func TestNodeUpdate(t *testing.T) { 179 | t.Run("logicalType updates the logical type of node", func(t *testing.T) { 180 | node := &zigbee.Node{} 181 | 182 | s := memory.New() 183 | logicalType(zigbee.EndDevice)(node, s) 184 | 185 | assert.Equal(t, zigbee.EndDevice, node.LogicalType) 186 | 187 | lt, ok := converter.Retrieve(s, "LogicalType", converter.LogicalTypeDecoder) 188 | 189 | assert.True(t, ok) 190 | assert.Equal(t, zigbee.EndDevice, lt) 191 | }) 192 | 193 | t.Run("lqi updates the lqi of node", func(t *testing.T) { 194 | node := &zigbee.Node{} 195 | 196 | s := memory.New() 197 | lqi(48)(node, s) 198 | 199 | assert.Equal(t, uint8(48), node.LQI) 200 | 201 | l, ok := s.UInt("LQI") 202 | assert.True(t, ok) 203 | assert.Equal(t, uint64(48), l) 204 | }) 205 | 206 | t.Run("depth updates the depth of node", func(t *testing.T) { 207 | node := &zigbee.Node{} 208 | 209 | s := memory.New() 210 | depth(3)(node, s) 211 | 212 | assert.Equal(t, uint8(3), node.Depth) 213 | 214 | d, ok := s.UInt("Depth") 215 | assert.True(t, ok) 216 | assert.Equal(t, uint64(3), d) 217 | }) 218 | 219 | t.Run("updateReceived updates the last received time of node", func(t *testing.T) { 220 | node := &zigbee.Node{} 221 | 222 | s := memory.New() 223 | updateReceived()(node, s) 224 | 225 | assert.NotEqual(t, time.Time{}, node.LastReceived) 226 | 227 | date, ok := converter.Retrieve(s, "LastReceived", converter.TimeDecoder) 228 | assert.True(t, ok) 229 | assert.True(t, time.Now().After(date)) 230 | }) 231 | 232 | t.Run("updateDiscovered updates the last received time of node", func(t *testing.T) { 233 | node := &zigbee.Node{} 234 | 235 | s := memory.New() 236 | updateDiscovered()(node, s) 237 | 238 | assert.NotEqual(t, time.Time{}, node.LastDiscovered) 239 | 240 | date, ok := converter.Retrieve(s, "LastDiscovered", converter.TimeDecoder) 241 | assert.True(t, ok) 242 | assert.True(t, time.Now().After(date)) 243 | }) 244 | } 245 | 246 | func TestNodeTable_Load(t *testing.T) { 247 | t.Run("loading a table from persistence contains expected nodes", func(t *testing.T) { 248 | s := memory.New() 249 | 250 | ieee := zigbee.GenerateLocalAdministeredIEEEAddress() 251 | 252 | time := time.UnixMilli(time.Now().UnixMilli()) 253 | 254 | nS := s.Section(ieee.String()) 255 | converter.Store(nS, "NetworkAddress", zigbee.NetworkAddress(0x1122), converter.NetworkAddressEncoder) 256 | converter.Store(nS, "LastReceived", time, converter.TimeEncoder) 257 | converter.Store(nS, "LastDiscovered", time, converter.TimeEncoder) 258 | converter.Store(nS, "LogicalType", zigbee.Router, converter.LogicalTypeEncoder) 259 | nS.Set("LQI", uint64(8)) 260 | nS.Set("Depth", uint64(2)) 261 | 262 | nt := newNodeTable(s) 263 | 264 | node, ok := nt.getByIEEE(ieee) 265 | assert.True(t, ok) 266 | assert.Equal(t, zigbee.NetworkAddress(0x1122), node.NetworkAddress) 267 | assert.Equal(t, time, node.LastDiscovered) 268 | assert.Equal(t, time, node.LastReceived) 269 | assert.Equal(t, uint8(8), node.LQI) 270 | assert.Equal(t, uint8(2), node.Depth) 271 | }) 272 | } 273 | -------------------------------------------------------------------------------- /node_unbind.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/shimmeringbee/zigbee" 7 | ) 8 | 9 | func (z *ZStack) UnbindNodeFromController(ctx context.Context, nodeAddress zigbee.IEEEAddress, sourceEndpoint zigbee.Endpoint, destinationEndpoint zigbee.Endpoint, cluster zigbee.ClusterID) error { 10 | networkAddress, err := z.ResolveNodeNWKAddress(ctx, nodeAddress) 11 | if err != nil { 12 | return nil 13 | } 14 | 15 | if err := z.sem.Acquire(ctx, 1); err != nil { 16 | return fmt.Errorf("failed to acquire semaphore: %w", err) 17 | } 18 | defer z.sem.Release(1) 19 | 20 | request := ZdoUnbindReq{ 21 | TargetAddress: networkAddress, 22 | SourceAddress: nodeAddress, 23 | SourceEndpoint: sourceEndpoint, 24 | ClusterID: cluster, 25 | DestinationAddressMode: 0x02, // Network Address (16 bits) 26 | DestinationAddress: uint64(0), 27 | DestinationEndpoint: destinationEndpoint, 28 | } 29 | 30 | _, err = z.nodeRequest(ctx, &request, &ZdoUnbindReqReply{}, &ZdoUnbindRsp{}, func(i interface{}) bool { 31 | msg := i.(*ZdoUnbindRsp) 32 | return msg.SourceAddress == networkAddress 33 | }) 34 | 35 | return err 36 | } 37 | 38 | type ZdoUnbindReq struct { 39 | TargetAddress zigbee.NetworkAddress 40 | SourceAddress zigbee.IEEEAddress 41 | SourceEndpoint zigbee.Endpoint 42 | ClusterID zigbee.ClusterID 43 | DestinationAddressMode uint8 44 | DestinationAddress uint64 45 | DestinationEndpoint zigbee.Endpoint 46 | } 47 | 48 | const ZdoUnbindReqID uint8 = 0x22 49 | 50 | type ZdoUnbindReqReply GenericZStackStatus 51 | 52 | func (r ZdoUnbindReqReply) WasSuccessful() bool { 53 | return r.Status == ZSuccess 54 | } 55 | 56 | const ZdoUnbindReqReplyID uint8 = 0x22 57 | 58 | type ZdoUnbindRsp struct { 59 | SourceAddress zigbee.NetworkAddress 60 | Status ZStackStatus 61 | } 62 | 63 | func (r ZdoUnbindRsp) WasSuccessful() bool { 64 | return r.Status == ZSuccess 65 | } 66 | 67 | const ZdoUnbindRspID uint8 = 0xa2 68 | -------------------------------------------------------------------------------- /node_unbind_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/bytecodec" 6 | "github.com/shimmeringbee/persistence/impl/memory" 7 | . "github.com/shimmeringbee/unpi" 8 | unpiTest "github.com/shimmeringbee/unpi/testing" 9 | "github.com/shimmeringbee/zigbee" 10 | "github.com/stretchr/testify/assert" 11 | "golang.org/x/sync/semaphore" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_UnbindToNode(t *testing.T) { 17 | t.Run("returns an success on query, response for requested network address is received", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 19 | defer cancel() 20 | 21 | unpiMock := unpiTest.NewMockAdapter() 22 | zstack := New(unpiMock, memory.New()) 23 | zstack.sem = semaphore.NewWeighted(8) 24 | defer unpiMock.Stop() 25 | 26 | call := unpiMock.On(SREQ, ZDO, ZdoUnbindReqReplyID).Return(Frame{ 27 | MessageType: SRSP, 28 | Subsystem: ZDO, 29 | CommandID: ZdoUnbindReqReplyID, 30 | Payload: []byte{0x00}, 31 | }) 32 | 33 | go func() { 34 | time.Sleep(10 * time.Millisecond) 35 | unpiMock.InjectOutgoing(Frame{ 36 | MessageType: AREQ, 37 | Subsystem: ZDO, 38 | CommandID: ZdoUnbindRspID, 39 | Payload: []byte{0x00, 0x40, 0x00}, 40 | }) 41 | }() 42 | 43 | zstack.nodeTable.addOrUpdate(zigbee.IEEEAddress(1), zigbee.NetworkAddress(0x4000)) 44 | 45 | err := zstack.UnbindNodeFromController(ctx, zigbee.IEEEAddress(1), 2, 4, 5) 46 | assert.NoError(t, err) 47 | 48 | UnbindReq := ZdoUnbindReq{} 49 | bytecodec.Unmarshal(call.CapturedCalls[0].Frame.Payload, &UnbindReq) 50 | 51 | assert.Equal(t, zigbee.NetworkAddress(0x4000), UnbindReq.TargetAddress) 52 | assert.Equal(t, zigbee.IEEEAddress(1), UnbindReq.SourceAddress) 53 | assert.Equal(t, zigbee.Endpoint(2), UnbindReq.SourceEndpoint) 54 | assert.Equal(t, uint64(0), UnbindReq.DestinationAddress) 55 | assert.Equal(t, zigbee.Endpoint(4), UnbindReq.DestinationEndpoint) 56 | assert.Equal(t, zigbee.ClusterID(0x5), UnbindReq.ClusterID) 57 | assert.Equal(t, uint8(0x02), UnbindReq.DestinationAddressMode) 58 | 59 | unpiMock.AssertCalls(t) 60 | }) 61 | } 62 | 63 | func Test_UnbindMessages(t *testing.T) { 64 | t.Run("verify ZdoUnbindReq marshals", func(t *testing.T) { 65 | req := ZdoUnbindReq{ 66 | TargetAddress: 0x2021, 67 | SourceAddress: zigbee.IEEEAddress(0x8899aabbccddeeff), 68 | SourceEndpoint: 0x01, 69 | ClusterID: 0xcafe, 70 | DestinationAddressMode: 0x01, 71 | DestinationAddress: 0x3ffe, 72 | DestinationEndpoint: 0x02, 73 | } 74 | 75 | data, err := bytecodec.Marshal(req) 76 | 77 | assert.NoError(t, err) 78 | assert.Equal(t, []byte{0x21, 0x20, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x01, 0xfe, 0xca, 0x01, 0xfe, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, data) 79 | }) 80 | 81 | t.Run("verify ZdoUnbindReqReply marshals", func(t *testing.T) { 82 | req := ZdoUnbindReqReply{ 83 | Status: 1, 84 | } 85 | 86 | data, err := bytecodec.Marshal(req) 87 | 88 | assert.NoError(t, err) 89 | assert.Equal(t, []byte{0x01}, data) 90 | }) 91 | 92 | t.Run("ZdoUnbindReqReply returns true if success", func(t *testing.T) { 93 | g := ZdoUnbindReqReply{Status: ZSuccess} 94 | assert.True(t, g.WasSuccessful()) 95 | }) 96 | 97 | t.Run("ZdoUnbindReqReply returns false if not success", func(t *testing.T) { 98 | g := ZdoUnbindReqReply{Status: ZFailure} 99 | assert.False(t, g.WasSuccessful()) 100 | }) 101 | 102 | t.Run("verify ZdoUnbindRsp marshals", func(t *testing.T) { 103 | req := ZdoUnbindRsp{ 104 | SourceAddress: zigbee.NetworkAddress(0x2000), 105 | Status: 1, 106 | } 107 | 108 | data, err := bytecodec.Marshal(req) 109 | 110 | assert.NoError(t, err) 111 | assert.Equal(t, []byte{0x00, 0x20, 0x01}, data) 112 | }) 113 | 114 | t.Run("ZdoUnbindRsp returns true if success", func(t *testing.T) { 115 | g := ZdoUnbindRsp{Status: ZSuccess} 116 | assert.True(t, g.WasSuccessful()) 117 | }) 118 | 119 | t.Run("ZdoUnbindRsp returns false if not success", func(t *testing.T) { 120 | g := ZdoUnbindRsp{Status: ZFailure} 121 | assert.False(t, g.WasSuccessful()) 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /nvram.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/shimmeringbee/bytecodec" 8 | "github.com/shimmeringbee/zigbee" 9 | "reflect" 10 | ) 11 | 12 | var NVRAMUnsuccessful = errors.New("nvram operation unsuccessful") 13 | var NVRAMUnrecognised = errors.New("nvram structure unrecognised") 14 | 15 | func (z *ZStack) writeNVRAM(ctx context.Context, v interface{}) error { 16 | configId, found := nvramStructToID[reflect.TypeOf(v)] 17 | 18 | if !found { 19 | return NVRAMUnrecognised 20 | } 21 | 22 | configValue, err := bytecodec.Marshal(v) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | writeRequest := SysOSALNVWrite{ 28 | NVItemID: configId, 29 | Offset: 0, 30 | Value: configValue, 31 | } 32 | 33 | writeResponse := SysOSALNVWriteReply{} 34 | 35 | if err := z.requestResponder.RequestResponse(ctx, writeRequest, &writeResponse); err != nil { 36 | return err 37 | } 38 | 39 | if writeResponse.Status != ZSuccess { 40 | return fmt.Errorf("%w: write: configId = %v, status = %v", NVRAMUnsuccessful, configId, writeResponse.Status) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (z *ZStack) readNVRAM(ctx context.Context, v interface{}) error { 47 | vType := reflect.TypeOf(v) 48 | 49 | if vType.Kind() == reflect.Ptr { 50 | vType = vType.Elem() 51 | } 52 | 53 | configId, found := nvramStructToID[vType] 54 | 55 | if !found { 56 | return NVRAMUnrecognised 57 | } 58 | 59 | readRequest := SysOSALNVRead{ 60 | NVItemID: configId, 61 | Offset: 0, 62 | } 63 | 64 | readResponse := SysOSALNVReadReply{} 65 | 66 | if err := z.requestResponder.RequestResponse(ctx, readRequest, &readResponse); err != nil { 67 | return err 68 | } 69 | 70 | if readResponse.Status != ZSuccess { 71 | return fmt.Errorf("%w: read: configId = %v, status = %v", NVRAMUnsuccessful, configId, readResponse.Status) 72 | } 73 | 74 | return bytecodec.Unmarshal(readResponse.Value, v) 75 | } 76 | 77 | type SysOSALNVWrite struct { 78 | NVItemID uint16 79 | Offset uint8 80 | Value []byte `bcsliceprefix:"8"` 81 | } 82 | 83 | const SysOSALNVWriteID uint8 = 0x09 84 | 85 | type SysOSALNVWriteReply GenericZStackStatus 86 | 87 | const SysOSALNVWriteReplyID uint8 = 0x09 88 | 89 | type SysOSALNVRead struct { 90 | NVItemID uint16 91 | Offset uint8 92 | } 93 | 94 | const SysOSALNVReadID uint8 = 0x08 95 | 96 | type SysOSALNVReadReply struct { 97 | Status ZStackStatus 98 | Value []byte `bcsliceprefix:"8"` 99 | } 100 | 101 | const SysOSALNVReadReplyID uint8 = 0x08 102 | 103 | var nvramStructToID = map[reflect.Type]uint16{ 104 | reflect.TypeOf(ZCDNVStartUpOption{}): ZCDNVStartUpOptionID, 105 | reflect.TypeOf(ZCDNVLogicalType{}): ZCDNVLogicalTypeID, 106 | reflect.TypeOf(ZCDNVSecurityMode{}): ZCDNVSecurityModeID, 107 | reflect.TypeOf(ZCDNVPreCfgKeysEnable{}): ZCDNVPreCfgKeysEnableID, 108 | reflect.TypeOf(ZCDNVPreCfgKey{}): ZCDNVPreCfgKeyID, 109 | reflect.TypeOf(ZCDNVZDODirectCB{}): ZCDNVZDODirectCBID, 110 | reflect.TypeOf(ZCDNVChanList{}): ZCDNVChanListID, 111 | reflect.TypeOf(ZCDNVPANID{}): ZCDNVPANIDID, 112 | reflect.TypeOf(ZCDNVExtPANID{}): ZCDNVExtPANIDID, 113 | reflect.TypeOf(ZCDNVUseDefaultTCLK{}): ZCDNVUseDefaultTCLKID, 114 | reflect.TypeOf(ZCDNVTCLKTableStart{}): ZCDNVTCLKTableStartID, 115 | } 116 | 117 | const ZCDNVStartUpOptionID uint16 = 0x0003 118 | 119 | type ZCDNVStartUpOption struct { 120 | StartOption uint8 121 | } 122 | 123 | const ZCDNVLogicalTypeID uint16 = 0x0087 124 | 125 | type ZCDNVLogicalType struct { 126 | LogicalType zigbee.LogicalType 127 | } 128 | 129 | const ZCDNVSecurityModeID uint16 = 0x0064 130 | 131 | type ZCDNVSecurityMode struct { 132 | Enabled uint8 133 | } 134 | 135 | const ZCDNVPreCfgKeysEnableID uint16 = 0x0063 136 | 137 | type ZCDNVPreCfgKeysEnable struct { 138 | Enabled uint8 139 | } 140 | 141 | const ZCDNVPreCfgKeyID uint16 = 0x0062 142 | 143 | type ZCDNVPreCfgKey struct { 144 | NetworkKey zigbee.NetworkKey 145 | } 146 | 147 | const ZCDNVZDODirectCBID uint16 = 0x008f 148 | 149 | type ZCDNVZDODirectCB struct { 150 | Enabled uint8 151 | } 152 | 153 | const ZCDNVChanListID uint16 = 0x0084 154 | 155 | type ZCDNVChanList struct { 156 | Channels [4]byte 157 | } 158 | 159 | const ZCDNVPANIDID uint16 = 0x0083 160 | 161 | type ZCDNVPANID struct { 162 | PANID zigbee.PANID 163 | } 164 | 165 | const ZCDNVExtPANIDID uint16 = 0x002d 166 | 167 | type ZCDNVExtPANID struct { 168 | ExtendedPANID zigbee.ExtendedPANID 169 | } 170 | 171 | const ZCDNVUseDefaultTCLKID uint16 = 0x006d 172 | 173 | type ZCDNVUseDefaultTCLK struct { 174 | Enabled uint8 175 | } 176 | 177 | const ZCDNVTCLKTableStartID uint16 = 0x0101 178 | 179 | type ZCDNVTCLKTableStart struct { 180 | Address zigbee.IEEEAddress 181 | NetworkKey zigbee.NetworkKey 182 | TXFrameCounter uint32 183 | RXFrameCounter uint32 184 | } 185 | -------------------------------------------------------------------------------- /nvram_test.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/shimmeringbee/bytecodec" 7 | "github.com/shimmeringbee/zigbee" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func Test_readNVRAM(t *testing.T) { 15 | t.Run("verifies that a request response is made to unpi", func(t *testing.T) { 16 | mrr := &ReturningMockRequestResponse{} 17 | 18 | z := ZStack{requestResponder: mrr} 19 | 20 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 21 | defer cancel() 22 | 23 | val := &ZCDNVLogicalType{} 24 | err := z.readNVRAM(ctx, val) 25 | 26 | assert.NoError(t, err) 27 | assert.Equal(t, zigbee.EndDevice, val.LogicalType) 28 | }) 29 | 30 | t.Run("verifies that read requests that fail raise an error", func(t *testing.T) { 31 | mrr := &ReadFailingMockRequestResponse{} 32 | 33 | z := ZStack{requestResponder: mrr} 34 | 35 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 36 | defer cancel() 37 | 38 | err := z.readNVRAM(ctx, &ZCDNVLogicalType{}) 39 | 40 | assert.Error(t, err) 41 | }) 42 | 43 | t.Run("verifies that a request response with errors is raised", func(t *testing.T) { 44 | mrr := new(MockRequestResponder) 45 | 46 | mrr.On("RequestResponse", mock.Anything, SysOSALNVRead{ 47 | NVItemID: ZCDNVLogicalTypeID, 48 | Offset: 0, 49 | }, &SysOSALNVReadReply{}).Return(errors.New("context expired")) 50 | 51 | z := ZStack{requestResponder: mrr} 52 | 53 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 54 | defer cancel() 55 | 56 | err := z.readNVRAM(ctx, &ZCDNVLogicalType{}) 57 | 58 | mrr.AssertExpectations(t) 59 | assert.Error(t, err) 60 | }) 61 | 62 | t.Run("verifies that unknown structure raises an error", func(t *testing.T) { 63 | mrr := new(MockRequestResponder) 64 | 65 | z := ZStack{requestResponder: mrr} 66 | 67 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 68 | defer cancel() 69 | 70 | err := z.readNVRAM(ctx, struct{}{}) 71 | 72 | mrr.AssertExpectations(t) 73 | assert.Error(t, err) 74 | }) 75 | } 76 | 77 | func Test_writeNVRAM(t *testing.T) { 78 | t.Run("verifies that a request response is made to unpi", func(t *testing.T) { 79 | mrr := new(MockRequestResponder) 80 | 81 | mrr.On("RequestResponse", mock.Anything, SysOSALNVWrite{ 82 | NVItemID: ZCDNVLogicalTypeID, 83 | Offset: 0, 84 | Value: []byte{0x02}, 85 | }, &SysOSALNVWriteReply{}).Return(nil) 86 | 87 | z := ZStack{requestResponder: mrr} 88 | 89 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 90 | defer cancel() 91 | 92 | err := z.writeNVRAM(ctx, ZCDNVLogicalType{LogicalType: zigbee.EndDevice}) 93 | 94 | mrr.AssertExpectations(t) 95 | assert.NoError(t, err) 96 | }) 97 | 98 | t.Run("verifies that write requests that fail raise an error", func(t *testing.T) { 99 | mrr := &WriteFailingMockRequestResponse{} 100 | 101 | z := ZStack{requestResponder: mrr} 102 | 103 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 104 | defer cancel() 105 | 106 | err := z.writeNVRAM(ctx, ZCDNVLogicalType{LogicalType: zigbee.EndDevice}) 107 | 108 | assert.Error(t, err) 109 | }) 110 | 111 | t.Run("verifies that a request response with errors is raised", func(t *testing.T) { 112 | mrr := new(MockRequestResponder) 113 | 114 | mrr.On("RequestResponse", mock.Anything, SysOSALNVWrite{ 115 | NVItemID: ZCDNVLogicalTypeID, 116 | Offset: 0, 117 | Value: []byte{0x02}, 118 | }, &SysOSALNVWriteReply{}).Return(errors.New("context expired")) 119 | 120 | z := ZStack{requestResponder: mrr} 121 | 122 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 123 | defer cancel() 124 | 125 | err := z.writeNVRAM(ctx, ZCDNVLogicalType{LogicalType: zigbee.EndDevice}) 126 | 127 | mrr.AssertExpectations(t) 128 | assert.Error(t, err) 129 | }) 130 | 131 | t.Run("verifies that unknown structure raises an error", func(t *testing.T) { 132 | mrr := new(MockRequestResponder) 133 | 134 | z := ZStack{requestResponder: mrr} 135 | 136 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 137 | defer cancel() 138 | 139 | err := z.writeNVRAM(ctx, struct{}{}) 140 | 141 | mrr.AssertExpectations(t) 142 | assert.Error(t, err) 143 | }) 144 | } 145 | 146 | type WriteFailingMockRequestResponse struct{} 147 | 148 | func (m *WriteFailingMockRequestResponse) RequestResponse(ctx context.Context, req interface{}, resp interface{}) error { 149 | response, ok := resp.(*SysOSALNVWriteReply) 150 | 151 | if !ok { 152 | panic("incorrect type passed to mock") 153 | } 154 | 155 | response.Status = 0x01 156 | 157 | return nil 158 | } 159 | 160 | type ReadFailingMockRequestResponse struct{} 161 | 162 | func (m *ReadFailingMockRequestResponse) RequestResponse(ctx context.Context, req interface{}, resp interface{}) error { 163 | response, ok := resp.(*SysOSALNVReadReply) 164 | 165 | if !ok { 166 | panic("incorrect type passed to mock") 167 | } 168 | 169 | response.Status = 0x01 170 | 171 | return nil 172 | } 173 | 174 | type ReturningMockRequestResponse struct{} 175 | 176 | func (m *ReturningMockRequestResponse) RequestResponse(ctx context.Context, req interface{}, resp interface{}) error { 177 | response, ok := resp.(*SysOSALNVReadReply) 178 | 179 | if !ok { 180 | panic("incorrect type passed to mock") 181 | } 182 | 183 | response.Status = ZSuccess 184 | response.Value = []byte{0x02} 185 | 186 | return nil 187 | } 188 | 189 | func Test_NVRAMStructs(t *testing.T) { 190 | t.Run("ZCDNVStartUpOption", func(t *testing.T) { 191 | s := ZCDNVStartUpOption{ 192 | StartOption: 3, 193 | } 194 | 195 | actualBytes, err := bytecodec.Marshal(s) 196 | 197 | expectedBytes := []byte{0x03} 198 | 199 | assert.NoError(t, err) 200 | assert.Equal(t, expectedBytes, actualBytes) 201 | }) 202 | 203 | t.Run("ZCDNVLogicalType", func(t *testing.T) { 204 | s := ZCDNVLogicalType{ 205 | LogicalType: zigbee.Router, 206 | } 207 | 208 | actualBytes, err := bytecodec.Marshal(s) 209 | 210 | expectedBytes := []byte{0x01} 211 | 212 | assert.NoError(t, err) 213 | assert.Equal(t, expectedBytes, actualBytes) 214 | }) 215 | 216 | t.Run("ZCDNVSecurityMode", func(t *testing.T) { 217 | s := ZCDNVSecurityMode{ 218 | Enabled: 1, 219 | } 220 | 221 | actualBytes, err := bytecodec.Marshal(s) 222 | 223 | expectedBytes := []byte{0x01} 224 | 225 | assert.NoError(t, err) 226 | assert.Equal(t, expectedBytes, actualBytes) 227 | }) 228 | 229 | t.Run("ZCDNVPreCfgKeysEnable", func(t *testing.T) { 230 | s := ZCDNVPreCfgKeysEnable{ 231 | Enabled: 1, 232 | } 233 | 234 | actualBytes, err := bytecodec.Marshal(s) 235 | 236 | expectedBytes := []byte{0x01} 237 | 238 | assert.NoError(t, err) 239 | assert.Equal(t, expectedBytes, actualBytes) 240 | }) 241 | 242 | t.Run("ZCDNVPreCfgKey", func(t *testing.T) { 243 | s := ZCDNVPreCfgKey{ 244 | NetworkKey: [16]byte{0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03}, 245 | } 246 | 247 | actualBytes, err := bytecodec.Marshal(s) 248 | 249 | expectedBytes := []byte{0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03} 250 | 251 | assert.NoError(t, err) 252 | assert.Equal(t, expectedBytes, actualBytes) 253 | }) 254 | 255 | t.Run("ZCDNVZDODirectCB", func(t *testing.T) { 256 | s := ZCDNVZDODirectCB{ 257 | Enabled: 1, 258 | } 259 | 260 | actualBytes, err := bytecodec.Marshal(s) 261 | 262 | expectedBytes := []byte{0x01} 263 | 264 | assert.NoError(t, err) 265 | assert.Equal(t, expectedBytes, actualBytes) 266 | }) 267 | 268 | t.Run("ZCDNVChanList", func(t *testing.T) { 269 | s := ZCDNVChanList{ 270 | Channels: [4]byte{0x00, 0x00, 0x00, 0x03}, 271 | } 272 | 273 | actualBytes, err := bytecodec.Marshal(s) 274 | 275 | expectedBytes := []byte{0x00, 0x00, 0x00, 0x03} 276 | 277 | assert.NoError(t, err) 278 | assert.Equal(t, expectedBytes, actualBytes) 279 | }) 280 | 281 | t.Run("ZCDNVPANID", func(t *testing.T) { 282 | s := ZCDNVPANID{ 283 | PANID: zigbee.PANID(0x0102), 284 | } 285 | 286 | actualBytes, err := bytecodec.Marshal(s) 287 | 288 | expectedBytes := []byte{0x02, 0x01} 289 | 290 | assert.NoError(t, err) 291 | assert.Equal(t, expectedBytes, actualBytes) 292 | }) 293 | 294 | t.Run("ZCDNVExtPANID", func(t *testing.T) { 295 | s := ZCDNVExtPANID{ 296 | ExtendedPANID: zigbee.ExtendedPANID(0x0102030405060708), 297 | } 298 | 299 | actualBytes, err := bytecodec.Marshal(s) 300 | 301 | expectedBytes := []byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01} 302 | 303 | assert.NoError(t, err) 304 | assert.Equal(t, expectedBytes, actualBytes) 305 | }) 306 | 307 | t.Run("ZCDNVUseDefaultTCLK", func(t *testing.T) { 308 | s := ZCDNVUseDefaultTCLK{ 309 | Enabled: 1, 310 | } 311 | 312 | actualBytes, err := bytecodec.Marshal(s) 313 | 314 | expectedBytes := []byte{0x01} 315 | 316 | assert.NoError(t, err) 317 | assert.Equal(t, expectedBytes, actualBytes) 318 | }) 319 | 320 | t.Run("ZCDNVTCLKTableStart", func(t *testing.T) { 321 | s := ZCDNVTCLKTableStart{ 322 | Address: zigbee.IEEEAddress(0x0807060504030201), 323 | NetworkKey: [16]byte{0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03}, 324 | TXFrameCounter: 123456, 325 | RXFrameCounter: 654321, 326 | } 327 | 328 | actualBytes, err := bytecodec.Marshal(s) 329 | 330 | expectedBytes := []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x0, 0x1, 0x2, 0x3, 0x0, 0x1, 0x2, 0x3, 0x0, 0x1, 0x2, 0x3, 0x0, 0x1, 0x2, 0x3, 0x40, 0xe2, 0x1, 0x0, 0xf1, 0xfb, 0x9, 0x0} 331 | 332 | assert.NoError(t, err) 333 | assert.Equal(t, expectedBytes, actualBytes) 334 | }) 335 | } 336 | -------------------------------------------------------------------------------- /zstack.go: -------------------------------------------------------------------------------- 1 | package zstack 2 | 3 | import ( 4 | "context" 5 | "github.com/shimmeringbee/logwrap" 6 | "github.com/shimmeringbee/logwrap/impl/golog" 7 | "github.com/shimmeringbee/persistence" 8 | "github.com/shimmeringbee/unpi/broker" 9 | "github.com/shimmeringbee/unpi/library" 10 | "github.com/shimmeringbee/zigbee" 11 | "golang.org/x/sync/semaphore" 12 | "io" 13 | "log" 14 | "os" 15 | "time" 16 | ) 17 | 18 | type RequestResponder interface { 19 | RequestResponse(ctx context.Context, req interface{}, resp interface{}) error 20 | } 21 | 22 | type Awaiter interface { 23 | Await(ctx context.Context, resp interface{}) error 24 | } 25 | 26 | type Subscriber interface { 27 | Subscribe(message interface{}, callback func(v interface{})) (error, func()) 28 | } 29 | 30 | type ZStack struct { 31 | requestResponder RequestResponder 32 | awaiter Awaiter 33 | subscriber Subscriber 34 | 35 | NetworkProperties NetworkProperties 36 | 37 | events chan interface{} 38 | 39 | networkManagerStop chan bool 40 | networkManagerIncoming chan interface{} 41 | 42 | messageReceiverStop func() 43 | 44 | nodeTable *nodeTable 45 | transactionIdStore chan uint8 46 | 47 | persistence persistence.Section 48 | 49 | sem *semaphore.Weighted 50 | 51 | logger logwrap.Logger 52 | } 53 | 54 | var _ zigbee.Provider = (*ZStack)(nil) 55 | 56 | type JoinState uint8 57 | 58 | const ( 59 | Off JoinState = 0x00 60 | OnCoordinator JoinState = 0x01 61 | OnAllRouters JoinState = 0x02 62 | ) 63 | 64 | type NetworkProperties struct { 65 | NetworkAddress zigbee.NetworkAddress 66 | IEEEAddress zigbee.IEEEAddress 67 | PANID zigbee.PANID 68 | ExtendedPANID zigbee.ExtendedPANID 69 | NetworkKey zigbee.NetworkKey 70 | Channel uint8 71 | JoinState JoinState 72 | } 73 | 74 | const DefaultZStackTimeout = 5 * time.Second 75 | const DefaultResolveIEEETimeout = 500 * time.Millisecond 76 | const DefaultZStackRetries = 3 77 | const DefaultInflightEvents = 50 78 | const DefaultInflightTransactions = 20 79 | 80 | func New(uart io.ReadWriter, p persistence.Section) *ZStack { 81 | ml := library.NewLibrary() 82 | registerMessages(ml) 83 | 84 | znp := broker.NewBroker(uart, uart, ml) 85 | znp.Start() 86 | 87 | transactionIDs := make(chan uint8, DefaultInflightTransactions) 88 | 89 | for i := range DefaultInflightTransactions { 90 | transactionIDs <- uint8(i) 91 | } 92 | 93 | zstack := &ZStack{ 94 | requestResponder: znp, 95 | awaiter: znp, 96 | subscriber: znp, 97 | events: make(chan interface{}, DefaultInflightEvents), 98 | networkManagerStop: make(chan bool, 1), 99 | networkManagerIncoming: make(chan interface{}, DefaultInflightEvents), 100 | nodeTable: newNodeTable(p.Section("Nodes")), 101 | transactionIdStore: transactionIDs, 102 | persistence: p, 103 | } 104 | 105 | zstack.WithGoLogger(log.New(os.Stderr, "", log.LstdFlags)) 106 | 107 | return zstack 108 | } 109 | 110 | func (z *ZStack) Stop() { 111 | z.stopNetworkManager() 112 | z.stopMessageReceiver() 113 | } 114 | 115 | func (z *ZStack) WithGoLogger(parentLogger *log.Logger) { 116 | z.logger = logwrap.New(golog.Wrap(parentLogger)) 117 | } 118 | 119 | func (z *ZStack) WithLogWrapLogger(parentLogger logwrap.Logger) { 120 | z.logger = parentLogger 121 | } 122 | --------------------------------------------------------------------------------