├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile.template ├── LICENSE ├── README.md ├── api ├── handlers.go ├── logger.go ├── router.go └── routes.go ├── application └── application.go ├── board ├── board.go ├── esp8266 │ └── esp8266.go ├── microbit │ └── microbit.go └── nrf51822dk │ └── nrf51822dk.go ├── config └── config.go ├── device ├── device.go ├── hook │ └── hook.go └── status │ └── status.go ├── glide.lock ├── glide.yaml ├── main.go ├── micro └── nrf51822 │ └── nrf51822.go ├── process ├── process.go └── status │ └── status.go ├── radio ├── bluetooth │ └── bluetooth.go └── wifi │ ├── nm.go │ └── wifi.go ├── release.sh ├── supervisor └── supervisor.go └── versionist.conf.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Extras 27 | vendor 28 | debug 29 | edge-node-manager 30 | 31 | # Editor stuff 32 | *.swo 33 | *.swp 34 | tags 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | os: linux 3 | sudo: false 4 | go: 1.8.x 5 | env: 6 | global: BUILD_OS=linux 7 | matrix: 8 | include: 9 | - env: ARCH=armel BUILD_ARCH=arm 10 | - env: ARCH=rpi BUILD_ARCH=arm 11 | - env: ARCH=armv7hf BUILD_ARCH=arm 12 | - env: ARCH=aarch64 BUILD_ARCH=arm64 13 | - env: ARCH=i386 BUILD_ARCH=386 14 | - env: ARCH=amd64 BUILD_ARCH=amd64 15 | before_install: go get github.com/Masterminds/glide 16 | install: glide install 17 | # This line builds the edge-node-manager binary and injects the current version tag 18 | # Note: $TRAVIS_TAG only works correctly outside of the quote marks 19 | script: GOARCH=$BUILD_ARCH GOOS=$BUILD_OS go build -ldflags '-X main.version='$TRAVIS_TAG 20 | before_deploy: 21 | - mkdir -p $TRAVIS_TAG 22 | - mv edge-node-manager $TRAVIS_TAG/edge-node-manager-$TRAVIS_TAG-$BUILD_OS-$ARCH 23 | deploy: 24 | - provider: s3 25 | access_key_id: AKIAI45R4ZZQ7YWCO47A 26 | secret_access_key: 27 | secure: fXEP9C2XFZMwTBHENxSRCYRnuKdvAd2XJepzyS81QM5s37AHedq0lZYUb9xzukxbpOhE36Ik97Ux08KDiZnyqkT+C97InNTXxmgUlvmcSZzhSoI+tEhMXPTmU8XbiiXnp5M+QbthEC7QQ/HZ1G0fEuiqmeX6gUeNcog0ZC3HgperPC0Rl1dgD9ntL6VRxvKWl3DLRt619y0NLG47v195uREfAsRlOiqa4CA2rmG0/4l0Vn9gchZlcwqlyBfezDlxgTwSPwAGoX1F2w5gZ8O7IAZGglzENXDx4HataUTVu1MPOsPV7Ki42FmVVajZvwKCcxAReRbnz5IFzkD3O2CUDnqZyn6Q8430G/HSk+jByyDVcvBcPm+VPim60veLeqTqXarVzUtMkHXQBMs8h8jC3iPDv8/NWAQzK9GtIUVmZSbem5g17P9kwwuyhOyrxJ4UVR1LAjJt8T5hwf9yN0Arkmf1wiE0VjHKAQkK6jS4/eiYzL05b8afrDsxcO4W4OIDxF2kWHpWKikfz96zmvyGVbxWCOiDYHvCaaeyN3aFLtDpGtAC8t7rx3DFzTE7hxBbVIg9vTRn6bEy3mX9dYP8SZ2YRR15PP7jWsMfDnsMiHZAwLG7pl7OqP6zxY/uUg+LPNuOP4QW52O+3MZ2ci8I69hqAg/E/ueTg09YgP+JWK8= 28 | skip_cleanup: true 29 | overwrite: true 30 | bucket: resin-production-downloads 31 | local-dir: $TRAVIS_TAG 32 | upload-dir: edge-node-manager/$TRAVIS_TAG 33 | acl: public_read 34 | on: 35 | branch: master 36 | tags: true 37 | after_deploy: ./release.sh "* [edge-node-manager-$TRAVIS_TAG-$BUILD_OS-$ARCH](https://resin-production-downloads.s3.amazonaws.com/edge-node-manager/$TRAVIS_TAG/edge-node-manager-$TRAVIS_TAG-$BUILD_OS-$ARCH)" 38 | notifications: 39 | email: false 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file 4 | automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! 5 | This project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## v3.0.0 - 2018-03-27 8 | 9 | * Update device name parameter to comply with API v4 and supervisor v7 #199 [Lucian Buzzo] 10 | 11 | ## v2.4.0 - 2017-09-08 12 | 13 | * Configure time between hotspot deletion/creation #193 [Joe Roberts] 14 | 15 | ## v2.3.0 - 2017-09-08 16 | 17 | * Configure time between supervisor checks #194 [Joe Roberts] 18 | 19 | ## v2.2.0 - 2017-08-29 20 | 21 | * Improve device endpoints #190 [Joe Roberts] 22 | 23 | ## v2.1.0 - 2017-08-18 24 | 25 | * Implement query dependent device endpoint #187 [Joe Roberts] 26 | 27 | ## v2.0.1 - 2017-08-18 28 | 29 | * Check for index-out-of-range #186 [Joe Roberts] 30 | 31 | ## v2.0.0 - 2017-08-14 32 | 33 | * Implement avahi/mdns discovery #185 [Joe Roberts] 34 | * Document config variables in the readme #185 [Joe Roberts] 35 | 36 | ## v1.6.2 - 2017-07-10 37 | 38 | * Updated readme [Joe Roberts] 39 | 40 | ## v1.6.1 - 2017-07-10 41 | 42 | * Implement context timeout for nmap scan [Joe Roberts] 43 | 44 | ## v1.6.0 - 2017-07-06 45 | 46 | * Tag architectures with the correct name [Joe Roberts] 47 | 48 | ## v1.5.3 - 2017-07-06 49 | 50 | * Only perform extra bluetooth initialisation if the device type is raspberrypi [Joe Roberts] 51 | 52 | ## v1.5.2 - 2017-07-06 53 | 54 | * Another attempt to fix conflicts between jobs [Joe Roberts] 55 | 56 | ## v1.5.1 - 2017-07-06 57 | 58 | * Fix issue with conflicting travis jobs [Joe Roberts] 59 | * Fix typo in variable check [Joe Roberts] 60 | 61 | ## v1.5.0 - 2017-07-06 62 | 63 | * Get architecture during the docker build [Joe Roberts] 64 | 65 | ## v1.4.5 - 2017-05-19 66 | 67 | * Add more ignores to .gitignore [John (Jack) Brown] 68 | 69 | ## v1.4.4 - 2017-05-19 70 | 71 | * Update `versionist` config to latest, ensuring semver types are case insensitive. [Heds Simons] 72 | 73 | ## v1.4.3 - 2017-04-25 74 | 75 | * Refactor hotspot creation [Joseph Roberts] 76 | * Removed unused resin-hotspot system connection [Joseph Roberts] 77 | * Update dependencies [Joseph Roberts] 78 | 79 | ## v1.4.2 - 2017-04-21 80 | 81 | * Ignore db open errors allowing the supervisor to silently retry [Joseph Roberts] 82 | 83 | ## v1.4.1 - 2017-04-21 84 | 85 | * Improve radio initialisation [Joseph Roberts] 86 | 87 | ## v1.4.0 - 2017-04-07 88 | 89 | * ESP Support [Joseph Roberts] 90 | 91 | ## v1.3.0 - 2017-03-23 92 | 93 | * Remove state from memory by writing to database [Joseph Roberts] 94 | 95 | ## v1.2.1 - 2017-03-16 96 | 97 | * Wrap timeout around all bluetooth operations [Joseph Roberts] 98 | 99 | ## v1.2.0 - 2017-03-16 100 | 101 | * Implement git auto release [Joseph Roberts] 102 | 103 | ## v1.1.0 - 2017-03-15 104 | 105 | * Implement out of date warning [Joseph Roberts] 106 | 107 | ## v1.0.3 - 2017-03-15 108 | 109 | * Remove edge-node-manager binary 110 | * Add edge-node-manager to .gitignore [Joseph Roberts] 111 | 112 | ## v1.0.2 - 2017-03-13 113 | 114 | * Sync changelog with release [Joseph Roberts] 115 | 116 | ## v1.0.1 - 2017-03-10 117 | 118 | * Get latest tag from the git API 119 | * Add update lock info to readme 120 | 121 | ## v1.0.0 - 2017-03-10 122 | 123 | * Set up versionist 124 | * Implement lock mechanism 125 | * Defer save state 126 | * Increased time to allow device to disconnect 127 | * Fix att-request-failed error 128 | * Quick hacky temporary fix to solve slow DFU 129 | * Remove disconnect requests as the dep. device disconnects on its own 130 | * Replace wget with curl 131 | * Temp fix device status by sending every minute 132 | * Retry updates 133 | * Bluetooth stability fixes 134 | * Add dep device log level 135 | * Removed timestamp so that the supervisor will set the timestamp instead 136 | 137 | ## v0.1.9 2017-03-01 138 | 139 | * Updated travis icon 140 | * Fixed nrf51822dk issue and added some debugging 141 | * Removed bluez apt source 142 | 143 | ## v0.1.8 2017-02-23 144 | 145 | * Updated readme 146 | * Updated dockerfile 147 | * Micro:bit now restarts after an update 148 | * Removed trailing new lines from log output 149 | * Changed default log level to Info 150 | * Updated gitignore 151 | * Removed images 152 | 153 | ## v0.1.7 2017-02-21 154 | 155 | * Removed resin sync from repo 156 | * Refactored bluetooth 157 | * Auto build binaries when a tag is created on master 158 | 159 | ## v0.1.6 2016-11-28 160 | 161 | * Merge pull request #86 from resin-io/develop 162 | 163 | ## v0.1.5 2016-11-01 164 | 165 | * Set up flag and logic to ensure is_online is always sent first time (#50) 166 | * 17 send progress dep device logs (#58) 167 | * Enforce order (#59) 168 | * Tidied logging (#60) 169 | * Turned off notifications 170 | * Fixing dep. device delete (#61) 171 | * 36 dep device env and config (#62) 172 | * 63 pull down fields (#64) 173 | * 36 dep device env and config (#65) 174 | 175 | ## v0.1.4 2016-10-26 176 | 177 | * Fixed device deletion (#44) 178 | * Auto board type (#45) 179 | * Auto board type (#46) 180 | 181 | ## v0.1.3 2016-10-25 182 | 183 | * Merge pluginification into develop 184 | * Custom device name issue#35 (#37) 185 | * Quick online state bug fix 186 | * Another quick bug fix 187 | * Microbit (#40) 188 | 189 | ## v0.1.2 2016-10-18 190 | 191 | * Merge develop into master 192 | 193 | ## v0.1.1 2016-10-11 194 | 195 | * Merge develop into master 196 | 197 | ## v0.1.0 2016-10-04 198 | 199 | * Updated git ignore 200 | * Merge initial-architecture into develop 201 | * Merge bluetooth-radio into develop 202 | * Merge godoc-comments into develop 203 | * Merge linting into develop 204 | * Merge proxyvisor-integration into develop 205 | * Merge docker into develop 206 | * Bumped docker version to 1.7 207 | * Refactored build method slightly 208 | * Merge testing into develop 209 | -------------------------------------------------------------------------------- /Dockerfile.template: -------------------------------------------------------------------------------- 1 | # Define a reusable argument containing the path to the go project 2 | # see: https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact 3 | ARG PROJECT_DIR=/go/src/github.com/resin-io/edge-node-manager 4 | 5 | # Build source using an image that has Go and Glide 6 | FROM billyteves/alpine-golang-glide:1.2.0 as GO 7 | 8 | ARG PROJECT_DIR 9 | 10 | RUN mkdir -p $PROJECT_DIR 11 | COPY . $PROJECT_DIR 12 | WORKDIR $PROJECT_DIR 13 | 14 | # Use Glide to install Go dependencies 15 | RUN glide install 16 | 17 | # Cross compile the ENM binary 18 | RUN env GOOS=linux GOARCH=arm go build 19 | 20 | # Debian base-image 21 | # See more about resin base images here: http://docs.resin.io/runtime/resin-base-images/ 22 | FROM resin/%%RESIN_MACHINE_NAME%%-debian 23 | 24 | ARG PROJECT_DIR 25 | 26 | # Disable systemd init system 27 | ENV INITSYSTEM off 28 | 29 | # Set our working directory 30 | WORKDIR /usr/src/app 31 | 32 | # The raspberrypi3 requires extra packages - `bluez-firmware` 33 | ENV EXTRA_PACKAGES "" 34 | RUN if [ "%%RESIN_MACHINE_NAME%%" = "raspberrypi3" ]; then export EXTRA_PACKAGES="bluez-firmware"; fi && \ 35 | apt-get update && apt-get install -yq --no-install-recommends \ 36 | $EXTRA_PACKAGES \ 37 | bluez \ 38 | curl \ 39 | jq && \ 40 | apt-get clean && rm -rf /var/lib/apt/lists/* 41 | 42 | # Copy the cross-compiled binary into the working directory 43 | COPY --from=GO $PROJECT_DIR/edge-node-manager ./ 44 | 45 | # The edge-node-manager will run when container starts up on the device 46 | CMD ["./edge-node-manager"] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 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. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # edge-node-manager 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/resin-io/edge-node-manager)](https://goreportcard.com/report/github.com/resin-io/edge-node-manager) 3 | [![Build Status](https://travis-ci.com/resin-io/edge-node-manager.svg?token=SsmNYChpKvn5yEXMkM2D&branch=master)](https://travis-ci.com/resin-io/edge-node-manager) 4 | 5 | resin.io dependent device edge-node-manager written in Go. 6 | 7 | ## Getting started 8 | - Sign up on [resin.io](https://dashboard.resin.io/signup) 9 | - Work through the [getting started 10 | guide](https://docs.resin.io/raspberrypi3/nodejs/getting-started/) 11 | - Create a new application 12 | - Set these variables in the `Fleet Configuration` application side tab 13 | - `RESIN_SUPERVISOR_DELTA=1` 14 | - `RESIN_UI_ENABLE_DEPENDENT_APPLICATIONS=1` 15 | - Clone this repository to your local workspace 16 | - Add the dependent application `resin remote` to your local workspace 17 | - Provision a gateway device 18 | - Push code to resin as normal :) 19 | - Follow the readme of the [supported dependent 20 | device](#supported-dependent-devices) you would like to use 21 | 22 | ## Configuration variables 23 | More info about environment variables can be found in 24 | the [documentation](https://docs.resin.io/management/env-vars/). If you 25 | don't set environment variables the default will be used. 26 | 27 | Environment Variable | Default | Description 28 | ------------ | ------------- | ------------- 29 | ENM_LOG_LEVEL | `info` | the edge-node-manager log level 30 | DEPENDENT_LOG_LEVEL | `info` | the dependent device log level 31 | ENM_SUPERVISOR_CHECK_DELAY | `1` | the time delay in seconds between each supervisor check at startup 32 | ENM_HOTSPOT_DELETE_DELAY | `10` | the time delay in seconds between hotspot deletion and creation 33 | ENM_CONFIG_LOOP_DELAY | `10` | the time delay in seconds between each application process loop 34 | ENM_CONFIG_PAUSE_DELAY | `10` | the time delay in seconds between each pause check 35 | ENM_HOTSPOT_SSID | `resin-hotspot` | the SSID used for the hotspot 36 | ENM_HOTSPOT_PASSWORD | `resin-hotspot` | the password used for the hotspot 37 | ENM_BLUETOOTH_SHORT_TIMEOUT | `1` | the timeout in seconds for instantaneous bluetooth operations 38 | ENM_BLUETOOTH_LONG_TIMEOUT | `10` | the timeout in seconds for long running bluetooth operations 39 | ENM_AVAHI_TIMEOUT | `10` | the timeout in seconds for Avahi scan operations 40 | ENM_UPDATE_RETRIES | `1` | the number of times the firmware update process should be retried 41 | ENM_ASSETS_DIRECTORY | `/data/assets` | the root directory used to store the dependent device firmware 42 | ENM_DB_DIRECTORY | `/data/database` | the root directory used to store the database 43 | ENM_DB_FILE | `enm.db` | the database file name 44 | ENM_API_VERSION | `v1` | the proxyvisor API version 45 | RESIN_SUPERVISOR_ADDRESS | `http://127.0.0.1:4000` | the address used to communicate with the proxyvisor 46 | RESIN_SUPERVISOR_API_KEY | `na` | the api key used to communicate with the proxyvisor 47 | ENM_LOCK_FILE_LOCATION | `/tmp/resin/resin-updates.lock` | the [lock file](https://github.com/resin-io/resin-supervisor/blob/master/docs/update-locking.md) location 48 | 49 | ## API 50 | The edge-node-manager provides an API that allows the user to set the 51 | target status of the main process. This is useful to free up the on-board radios 52 | allowing user code to interact directly with the dependent devices e.g. to 53 | collect sensor data. 54 | 55 | **Warning** - Do not try and interact with the on-board radios whilst the 56 | edge-node-manager is running (this leads to inconsistent, unexpected behaviour). 57 | 58 | ### SET /v1/enm/status 59 | Set the edge-node-manager process status. 60 | 61 | #### Example 62 | ``` 63 | curl -i -H "Content-Type: application/json" -X PUT --data \ 64 | '{"targetStatus":"Paused"}' localhost:1337/v1/enm/status 65 | 66 | curl -i -H "Content-Type: application/json" -X PUT --data \ 67 | '{"targetStatus":"Running"}' localhost:1337/v1/enm/status 68 | ``` 69 | 70 | #### Response 71 | ``` 72 | HTTP/1.1 200 OK 73 | ``` 74 | 75 | ### GET /v1/enm/status 76 | Get the edge-node-manager process status. 77 | 78 | #### Example 79 | ``` 80 | curl -i -X GET localhost:1337/v1/enm/status 81 | ``` 82 | 83 | #### Response 84 | ``` 85 | HTTP/1.1 200 OK 86 | { 87 | "currentStatus":"Running", 88 | "targetStatus":"Paused", 89 | } 90 | ``` 91 | 92 | ### GET /v1/devices 93 | Get all dependent devices. 94 | 95 | #### Example 96 | ``` 97 | curl -i -X GET localhost:1337/v1/devices 98 | ``` 99 | 100 | #### Response 101 | ``` 102 | HTTP/1.1 200 OK 103 | [{ 104 | "ApplicationUUID": 511898, 105 | "BoardType": "esp8266", 106 | "Name": "holy-sunset", 107 | "LocalUUID": "1265892", 108 | "ResinUUID": "64a1ae375b213d7e5af8409da3ad63108df4c8462089a05aa9af358c3f0df1", 109 | "Commit": "16b5cd4df8085d2872a6f6fc0c378629a185d78b", 110 | "TargetCommit": "16b5cd4df8085d2872a6f6fc0c378629a185d78b", 111 | "Status": "Idle", 112 | "Config": null, 113 | "TargetConfig": { 114 | "RESIN_HOST_TYPE": "esp8266", 115 | "RESIN_SUPERVISOR_DELTA": "1" 116 | }, 117 | "Environment": null, 118 | "TargetEnvironment": {}, 119 | "RestartFlag": false, 120 | "DeleteFlag": false 121 | }] 122 | ``` 123 | 124 | ### GET /v1/devices/{uuid} 125 | Get a dependent device. 126 | 127 | #### Example 128 | ``` 129 | curl -i -X GET localhost:1337/v1/devices/1265892 130 | ``` 131 | 132 | #### Response 133 | ``` 134 | HTTP/1.1 200 OK 135 | { 136 | "ApplicationUUID": 511898, 137 | "BoardType": "esp8266", 138 | "Name": "holy-sunset", 139 | "LocalUUID": "1265892", 140 | "ResinUUID": "64a1ae375b213d7e5af8409da3ad63108df4c8462089a05aa9af358c3f0df1", 141 | "Commit": "16b5cd4df8085d2872a6f6fc0c378629a185d78b", 142 | "TargetCommit": "16b5cd4df8085d2872a6f6fc0c378629a185d78b", 143 | "Status": "Idle", 144 | "Config": null, 145 | "TargetConfig": { 146 | "RESIN_HOST_TYPE": "esp8266", 147 | "RESIN_SUPERVISOR_DELTA": "1" 148 | }, 149 | "Environment": null, 150 | "TargetEnvironment": {}, 151 | "RestartFlag": false, 152 | "DeleteFlag": false 153 | } 154 | ``` 155 | 156 | ## Supported dependent devices 157 | - [micro:bit](https://github.com/resin-io-projects/micro-bit) 158 | - [nRF51822-DK](https://github.com/resin-io-projects/nRF51822-DK) 159 | - [ESP8266](https://github.com/resin-io-projects/esp8266) 160 | 161 | ## Further reading 162 | ### About 163 | The edge-node-manager is an example of a gateway 164 | application designed to bridge the gap between Resin OS capable single board 165 | computers (e.g. the Raspberry Pi) and non Resin OS capable devices (e.g. 166 | micro-controllers). It has been designed to make it as easy as possible to add 167 | new supported dependent device types and to run alongside your user application. 168 | 169 | The following functionality is implemented: 170 | - Dependent device detection 171 | - Dependent device provisioning 172 | - Dependent device restart 173 | - Dependent device over-the-air (OTA) updating 174 | - Dependent device logging and information updating 175 | - API 176 | 177 | ### Definitions 178 | #### Dependent application 179 | A dependent application is a Resin application that targets devices not capable 180 | of interacting directly with the Resin API. 181 | 182 | The dependent application is scoped under a Resin application, which gets the 183 | definition of gateway application. 184 | 185 | A dependent application follows the same development cycle as a conventional 186 | Resin application: 187 | - It binds to your git workspace via the `resin remote` 188 | - It consists of a Docker application 189 | - It offers the same environment and configuration variables management 190 | 191 | There are some key differences: 192 | - It does not support Dockerfile templating 193 | - The Dockerfile must target an x86 base image 194 | - The actual firmware must be stored in the `/assets` folder within the built 195 | docker image 196 | 197 | #### Dependent device 198 | A dependent device is a device not capable of interacting directly with the 199 | Resin API - the reasons can be several, the most common are: 200 | - No direct Internet capabilities 201 | - Not able to run the Resin OS (being a microcontroller, for example) 202 | 203 | #### Gateway application 204 | The gateway application is responsible for detecting, provisioning and managing 205 | dependent devices belonging to one of its dependent applications. This is 206 | possible leveraging a new set of endpoints exposed by the [Resin 207 | Supervisor](https://github.com/resin-io/resin-supervisor). 208 | 209 | The edge-node-manager (this repository) is an example of a gateway application. 210 | 211 | #### Gateway device 212 | The gateway device runs the gateway application and has the needed on-board 213 | radios to communicate with the managed dependent devices, for example: 214 | - Bluetooth 215 | - WiFi 216 | - LoRa 217 | - ZigBee 218 | 219 | Throughout development a Raspberry Pi 3 has been used as the gateway device. 220 | -------------------------------------------------------------------------------- /api/handlers.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/asdine/storm" 8 | "github.com/asdine/storm/q" 9 | "github.com/gorilla/mux" 10 | "github.com/resin-io/edge-node-manager/config" 11 | "github.com/resin-io/edge-node-manager/device" 12 | "github.com/resin-io/edge-node-manager/process" 13 | "github.com/resin-io/edge-node-manager/process/status" 14 | 15 | log "github.com/Sirupsen/logrus" 16 | ) 17 | 18 | func DependentDeviceUpdate(w http.ResponseWriter, r *http.Request) { 19 | type dependentDeviceUpdate struct { 20 | Commit string `json:"commit"` 21 | Environment interface{} `json:"environment"` 22 | } 23 | 24 | decoder := json.NewDecoder(r.Body) 25 | var content dependentDeviceUpdate 26 | if err := decoder.Decode(&content); err != nil { 27 | log.WithFields(log.Fields{ 28 | "Error": err, 29 | }).Error("Unable to decode Dependent device update hook") 30 | w.WriteHeader(http.StatusInternalServerError) 31 | return 32 | } 33 | 34 | if err := setField(r, "TargetCommit", content.Commit); err != nil { 35 | w.WriteHeader(http.StatusInternalServerError) 36 | return 37 | } 38 | 39 | w.WriteHeader(http.StatusAccepted) 40 | } 41 | 42 | func DependentDeviceDelete(w http.ResponseWriter, r *http.Request) { 43 | if err := setField(r, "Delete", true); err != nil { 44 | w.WriteHeader(http.StatusInternalServerError) 45 | return 46 | } 47 | 48 | w.WriteHeader(http.StatusOK) 49 | } 50 | 51 | func DependentDeviceRestart(w http.ResponseWriter, r *http.Request) { 52 | if err := setField(r, "Restart", true); err != nil { 53 | w.WriteHeader(http.StatusInternalServerError) 54 | return 55 | } 56 | 57 | w.WriteHeader(http.StatusOK) 58 | } 59 | 60 | func DependentDevicesQuery(w http.ResponseWriter, r *http.Request) { 61 | db, err := storm.Open(config.GetDbPath()) 62 | if err != nil { 63 | w.WriteHeader(http.StatusInternalServerError) 64 | return 65 | } 66 | defer db.Close() 67 | 68 | var d []device.Device 69 | if err := db.All(&d); err != nil { 70 | log.WithFields(log.Fields{ 71 | "Error": err, 72 | }).Error("Unable to find devices in database") 73 | w.WriteHeader(http.StatusInternalServerError) 74 | return 75 | } 76 | 77 | bytes, err := json.Marshal(d) 78 | if err != nil { 79 | log.WithFields(log.Fields{ 80 | "Error": err, 81 | }).Error("Unable to encode devices") 82 | w.WriteHeader(http.StatusInternalServerError) 83 | return 84 | } 85 | 86 | w.Header().Set("Content-Type", "application/json") 87 | if written, err := w.Write(bytes); (err != nil) || (written != len(bytes)) { 88 | log.WithFields(log.Fields{ 89 | "Error": err, 90 | }).Error("Unable to write response") 91 | w.WriteHeader(http.StatusInternalServerError) 92 | return 93 | } 94 | 95 | log.Debug("Get dependent device") 96 | } 97 | 98 | func DependentDeviceQuery(w http.ResponseWriter, r *http.Request) { 99 | vars := mux.Vars(r) 100 | UUID := vars["uuid"] 101 | 102 | db, err := storm.Open(config.GetDbPath()) 103 | if err != nil { 104 | w.WriteHeader(http.StatusInternalServerError) 105 | return 106 | } 107 | defer db.Close() 108 | 109 | var d device.Device 110 | if err := db.Select( 111 | q.Or( 112 | q.Eq("LocalUUID", UUID), 113 | q.Eq("ResinUUID", UUID), 114 | ), 115 | ).First(&d); err != nil { 116 | log.WithFields(log.Fields{ 117 | "Error": err, 118 | "UUID": UUID, 119 | }).Error("Unable to find device in database") 120 | w.WriteHeader(http.StatusInternalServerError) 121 | return 122 | } 123 | 124 | bytes, err := json.Marshal(d) 125 | if err != nil { 126 | log.WithFields(log.Fields{ 127 | "Error": err, 128 | }).Error("Unable to encode device") 129 | w.WriteHeader(http.StatusInternalServerError) 130 | return 131 | } 132 | 133 | w.Header().Set("Content-Type", "application/json") 134 | if written, err := w.Write(bytes); (err != nil) || (written != len(bytes)) { 135 | log.WithFields(log.Fields{ 136 | "Error": err, 137 | }).Error("Unable to write response") 138 | w.WriteHeader(http.StatusInternalServerError) 139 | return 140 | } 141 | 142 | log.WithFields(log.Fields{ 143 | "Device": d, 144 | }).Debug("Get dependent device") 145 | } 146 | 147 | func SetStatus(w http.ResponseWriter, r *http.Request) { 148 | type s struct { 149 | TargetStatus status.Status `json:"targetStatus"` 150 | } 151 | 152 | var content *s 153 | decoder := json.NewDecoder(r.Body) 154 | if err := decoder.Decode(&content); err != nil { 155 | log.WithFields(log.Fields{ 156 | "Error": err, 157 | }).Error("Unable to decode status hook") 158 | w.WriteHeader(http.StatusInternalServerError) 159 | return 160 | } 161 | 162 | process.TargetStatus = content.TargetStatus 163 | 164 | w.WriteHeader(http.StatusOK) 165 | 166 | log.WithFields(log.Fields{ 167 | "Target status": process.TargetStatus, 168 | }).Debug("Set status") 169 | } 170 | 171 | func GetStatus(w http.ResponseWriter, r *http.Request) { 172 | type s struct { 173 | CurrentStatus status.Status `json:"currentStatus"` 174 | TargetStatus status.Status `json:"targetStatus"` 175 | } 176 | 177 | content := &s{ 178 | CurrentStatus: process.CurrentStatus, 179 | TargetStatus: process.TargetStatus, 180 | } 181 | 182 | bytes, err := json.Marshal(content) 183 | if err != nil { 184 | log.WithFields(log.Fields{ 185 | "Error": err, 186 | }).Error("Unable to encode status hook") 187 | w.WriteHeader(http.StatusInternalServerError) 188 | return 189 | } 190 | 191 | w.Header().Set("Content-Type", "application/json") 192 | if written, err := w.Write(bytes); (err != nil) || (written != len(bytes)) { 193 | log.WithFields(log.Fields{ 194 | "Error": err, 195 | }).Error("Unable to write response") 196 | w.WriteHeader(http.StatusInternalServerError) 197 | return 198 | } 199 | 200 | log.WithFields(log.Fields{ 201 | "Target status": process.TargetStatus, 202 | "Curent status": process.CurrentStatus, 203 | }).Debug("Get status") 204 | } 205 | 206 | func setField(r *http.Request, key string, value interface{}) error { 207 | vars := mux.Vars(r) 208 | deviceUUID := vars["uuid"] 209 | 210 | db, err := storm.Open(config.GetDbPath()) 211 | if err != nil { 212 | return err 213 | } 214 | defer db.Close() 215 | 216 | var d device.Device 217 | if err := db.One("ResinUUID", deviceUUID, &d); err != nil { 218 | log.WithFields(log.Fields{ 219 | "Error": err, 220 | "UUID": deviceUUID, 221 | }).Error("Unable to find device in database") 222 | return err 223 | } 224 | 225 | switch key { 226 | case "TargetCommit": 227 | d.TargetCommit = value.(string) 228 | case "Delete": 229 | d.DeleteFlag = value.(bool) 230 | case "Restart": 231 | d.RestartFlag = value.(bool) 232 | default: 233 | log.WithFields(log.Fields{ 234 | "Error": err, 235 | "UUID": deviceUUID, 236 | "Key": key, 237 | "value": value, 238 | }).Error("Unable to set field") 239 | return err 240 | } 241 | 242 | if err := db.Update(&d); err != nil { 243 | log.WithFields(log.Fields{ 244 | "Error": err, 245 | "UUID": deviceUUID, 246 | }).Error("Unable to update device in database") 247 | return err 248 | } 249 | 250 | log.WithFields(log.Fields{ 251 | "UUID": deviceUUID, 252 | "Key": key, 253 | "value": value, 254 | }).Debug("Dependent device field updated") 255 | 256 | return nil 257 | } 258 | -------------------------------------------------------------------------------- /api/logger.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | log "github.com/Sirupsen/logrus" 8 | ) 9 | 10 | // Logger logs API requests 11 | func Logger(inner http.Handler, name string) http.Handler { 12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | start := time.Now() 14 | 15 | inner.ServeHTTP(w, r) 16 | 17 | log.WithFields(log.Fields{ 18 | "Method": r.Method, 19 | "Request URI": r.RequestURI, 20 | "Name": name, 21 | "Time": time.Since(start), 22 | }).Debug("API request") 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /api/router.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | ) 8 | 9 | // NewRouter creates a router to route API requests 10 | func NewRouter() *mux.Router { 11 | router := mux.NewRouter().StrictSlash(true) 12 | for _, route := range routes { 13 | var handler http.Handler 14 | handler = route.HandlerFunc 15 | handler = Logger(handler, route.Name) 16 | 17 | router. 18 | Methods(route.Method). 19 | Path(route.Pattern). 20 | Name(route.Name). 21 | Handler(handler) 22 | } 23 | return router 24 | } 25 | -------------------------------------------------------------------------------- /api/routes.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "net/http" 4 | 5 | // Route contains all the variables needed to define a route 6 | type Route struct { 7 | Name string 8 | Method string 9 | Pattern string 10 | HandlerFunc http.HandlerFunc 11 | } 12 | 13 | // Routes holds all the routes assigned to the API 14 | type Routes []Route 15 | 16 | var routes = Routes{ 17 | Route{ 18 | "DependentDeviceUpdate", 19 | "PUT", 20 | "/v1/devices/{uuid}", 21 | DependentDeviceUpdate, 22 | }, 23 | Route{ 24 | "DependentDeviceDelete", 25 | "DELETE", 26 | "/v1/devices/{uuid}", 27 | DependentDeviceDelete, 28 | }, 29 | Route{ 30 | "DependentDeviceRestart", 31 | "PUT", 32 | "/v1/devices/{uuid}/restart", 33 | DependentDeviceRestart, 34 | }, 35 | Route{ 36 | "DependentDevicesQuery", 37 | "GET", 38 | "/v1/devices", 39 | DependentDevicesQuery, 40 | }, 41 | Route{ 42 | "DependentDeviceQuery", 43 | "GET", 44 | "/v1/devices/{uuid}", 45 | DependentDeviceQuery, 46 | }, 47 | Route{ 48 | "SetStatus", 49 | "PUT", 50 | "/v1/enm/status", 51 | SetStatus, 52 | }, 53 | Route{ 54 | "GetStatus", 55 | "GET", 56 | "/v1/enm/status", 57 | GetStatus, 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /application/application.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/resin-io/edge-node-manager/board" 8 | "github.com/resin-io/edge-node-manager/board/esp8266" 9 | "github.com/resin-io/edge-node-manager/board/microbit" 10 | "github.com/resin-io/edge-node-manager/board/nrf51822dk" 11 | ) 12 | 13 | type Application struct { 14 | Board board.Interface `json:"-"` 15 | BoardType board.Type `json:"-"` 16 | Name string `json:"name"` 17 | ResinUUID int `json:"id"` 18 | Commit string `json:"-"` // Ignore this when unmarshalling from the supervisor as we want to set the target commit 19 | TargetCommit string `json:"commit"` // Set json tag to commit as the supervisor has no concept of target commit 20 | Config map[string]interface{} `json:"config"` 21 | } 22 | 23 | func (a Application) String() string { 24 | return fmt.Sprintf( 25 | "Board type: %s, "+ 26 | "Name: %s, "+ 27 | "Resin UUID: %d, "+ 28 | "Commit: %s, "+ 29 | "Target commit: %s, "+ 30 | "Config: %v", 31 | a.BoardType, 32 | a.Name, 33 | a.ResinUUID, 34 | a.Commit, 35 | a.TargetCommit, 36 | a.Config) 37 | } 38 | 39 | func Unmarshal(bytes []byte) (map[int]Application, error) { 40 | applications := make(map[int]Application) 41 | 42 | var buffer []Application 43 | if err := json.Unmarshal(bytes, &buffer); err != nil { 44 | return nil, err 45 | } 46 | 47 | for key, value := range buffer { 48 | value, ok := value.Config["RESIN_HOST_TYPE"] 49 | if !ok { 50 | continue 51 | } 52 | boardType := (board.Type)(value.(string)) 53 | 54 | var b board.Interface 55 | switch boardType { 56 | case board.MICROBIT: 57 | b = microbit.Microbit{} 58 | case board.NRF51822DK: 59 | b = nrf51822dk.Nrf51822dk{} 60 | case board.ESP8266: 61 | b = esp8266.Esp8266{} 62 | default: 63 | continue 64 | } 65 | 66 | application := buffer[key] 67 | application.BoardType = boardType 68 | application.Board = b 69 | 70 | applications[application.ResinUUID] = application 71 | } 72 | 73 | return applications, nil 74 | } 75 | -------------------------------------------------------------------------------- /board/board.go: -------------------------------------------------------------------------------- 1 | package board 2 | 3 | type Type string 4 | 5 | const ( 6 | MICROBIT Type = "microbit" 7 | NRF51822DK = "nrf51822dk" 8 | ESP8266 = "esp8266" 9 | ) 10 | 11 | type Interface interface { 12 | InitialiseRadio() error 13 | CleanupRadio() error 14 | Update(filePath string) error 15 | Scan(applicationUUID int) (map[string]struct{}, error) 16 | Online() (bool, error) 17 | Restart() error 18 | Identify() error 19 | UpdateConfig(interface{}) error 20 | UpdateEnvironment(interface{}) error 21 | } 22 | -------------------------------------------------------------------------------- /board/esp8266/esp8266.go: -------------------------------------------------------------------------------- 1 | package esp8266 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strconv" 7 | 8 | log "github.com/Sirupsen/logrus" 9 | "github.com/resin-io/edge-node-manager/radio/wifi" 10 | ) 11 | 12 | type Esp8266 struct { 13 | Log *log.Logger 14 | LocalUUID string 15 | } 16 | 17 | func (b Esp8266) InitialiseRadio() error { 18 | return wifi.Initialise() 19 | } 20 | 21 | func (b Esp8266) CleanupRadio() error { 22 | return wifi.Cleanup() 23 | } 24 | 25 | func (b Esp8266) Update(filePath string) error { 26 | b.Log.Info("Starting update") 27 | 28 | ip, err := wifi.GetIP(b.LocalUUID) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | if err := wifi.PostForm("http://"+ip+"/update", path.Join(filePath, "firmware.bin")); err != nil { 34 | return err 35 | } 36 | 37 | b.Log.Info("Finished update") 38 | 39 | return nil 40 | } 41 | 42 | func (b Esp8266) Scan(applicationUUID int) (map[string]struct{}, error) { 43 | return wifi.Scan(strconv.Itoa(applicationUUID)) 44 | } 45 | 46 | func (b Esp8266) Online() (bool, error) { 47 | return wifi.Online(b.LocalUUID) 48 | } 49 | 50 | func (b Esp8266) Restart() error { 51 | b.Log.Info("Restarting...") 52 | return fmt.Errorf("Restart not implemented") 53 | } 54 | 55 | func (b Esp8266) Identify() error { 56 | b.Log.Info("Identifying...") 57 | return fmt.Errorf("Identify not implemented") 58 | } 59 | 60 | func (b Esp8266) UpdateConfig(config interface{}) error { 61 | b.Log.WithFields(log.Fields{ 62 | "Config": config, 63 | }).Info("Updating config...") 64 | return fmt.Errorf("Update config not implemented") 65 | } 66 | 67 | func (b Esp8266) UpdateEnvironment(config interface{}) error { 68 | b.Log.WithFields(log.Fields{ 69 | "Config": config, 70 | }).Info("Updating environment...") 71 | return fmt.Errorf("Update environment not implemented") 72 | } 73 | -------------------------------------------------------------------------------- /board/microbit/microbit.go: -------------------------------------------------------------------------------- 1 | package microbit 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | log "github.com/Sirupsen/logrus" 9 | "github.com/currantlabs/ble" 10 | "github.com/resin-io/edge-node-manager/config" 11 | "github.com/resin-io/edge-node-manager/micro/nrf51822" 12 | "github.com/resin-io/edge-node-manager/radio/bluetooth" 13 | ) 14 | 15 | type Microbit struct { 16 | Log *log.Logger 17 | Micro nrf51822.Nrf51822 18 | } 19 | 20 | var ( 21 | dfu *ble.Characteristic 22 | shortTimeout time.Duration 23 | ) 24 | 25 | func (b Microbit) InitialiseRadio() error { 26 | return b.Micro.InitialiseRadio() 27 | } 28 | 29 | func (b Microbit) CleanupRadio() error { 30 | return b.Micro.CleanupRadio() 31 | } 32 | 33 | func (b Microbit) Update(filePath string) error { 34 | b.Log.Info("Starting update") 35 | 36 | if err := b.Micro.ExtractFirmware(filePath, "micro-bit.bin", "micro-bit.dat"); err != nil { 37 | return err 38 | } 39 | 40 | name, err := bluetooth.GetName(b.Micro.LocalUUID) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | if name != "DfuTarg" { 46 | b.Log.Debug("Starting bootloader") 47 | 48 | client, err := bluetooth.Connect(b.Micro.LocalUUID) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | // Ignore the error because this command causes the device to disconnect 54 | bluetooth.WriteCharacteristic(client, dfu, []byte{nrf51822.Start}, false) 55 | 56 | // Give the device time to disconnect 57 | time.Sleep(shortTimeout) 58 | 59 | b.Log.Debug("Started bootloader") 60 | } else { 61 | b.Log.Debug("Bootloader already started") 62 | } 63 | 64 | client, err := bluetooth.Connect(b.Micro.LocalUUID) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if err := b.Micro.Update(client); err != nil { 70 | return err 71 | } 72 | 73 | b.Log.Info("Finished update") 74 | 75 | return nil 76 | } 77 | 78 | func (b Microbit) Scan(applicationUUID int) (map[string]struct{}, error) { 79 | id := "BBC micro:bit [" + strconv.Itoa(applicationUUID) + "]" 80 | return bluetooth.Scan(id) 81 | } 82 | 83 | func (b Microbit) Online() (bool, error) { 84 | return bluetooth.Online(b.Micro.LocalUUID) 85 | } 86 | 87 | func (b Microbit) Restart() error { 88 | b.Log.Info("Restarting...") 89 | return fmt.Errorf("Restart not implemented") 90 | } 91 | 92 | func (b Microbit) Identify() error { 93 | b.Log.Info("Identifying...") 94 | return fmt.Errorf("Identify not implemented") 95 | } 96 | 97 | func (b Microbit) UpdateConfig(config interface{}) error { 98 | b.Log.WithFields(log.Fields{ 99 | "Config": config, 100 | }).Info("Updating config...") 101 | return fmt.Errorf("Update config not implemented") 102 | } 103 | 104 | func (b Microbit) UpdateEnvironment(config interface{}) error { 105 | b.Log.WithFields(log.Fields{ 106 | "Config": config, 107 | }).Info("Updating environment...") 108 | return fmt.Errorf("Update environment not implemented") 109 | } 110 | 111 | func init() { 112 | log.SetLevel(config.GetLogLevel()) 113 | 114 | var err error 115 | if shortTimeout, err = config.GetShortBluetoothTimeout(); err != nil { 116 | log.WithFields(log.Fields{ 117 | "Error": err, 118 | }).Fatal("Unable to load bluetooth timeout") 119 | } 120 | 121 | dfu, err = bluetooth.GetCharacteristic("e95d93b1251d470aa062fa1922dfa9a8", ble.CharRead+ble.CharWrite, 0x0D, 0x0E) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | 126 | log.Debug("Initialised micro:bit characteristics") 127 | } 128 | -------------------------------------------------------------------------------- /board/nrf51822dk/nrf51822dk.go: -------------------------------------------------------------------------------- 1 | package nrf51822dk 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | log "github.com/Sirupsen/logrus" 9 | "github.com/currantlabs/ble" 10 | "github.com/resin-io/edge-node-manager/config" 11 | "github.com/resin-io/edge-node-manager/micro/nrf51822" 12 | "github.com/resin-io/edge-node-manager/radio/bluetooth" 13 | ) 14 | 15 | type Nrf51822dk struct { 16 | Log *log.Logger 17 | Micro nrf51822.Nrf51822 18 | } 19 | 20 | var ( 21 | dfu *ble.Characteristic 22 | shortTimeout time.Duration 23 | ) 24 | 25 | func (b Nrf51822dk) InitialiseRadio() error { 26 | return b.Micro.InitialiseRadio() 27 | } 28 | 29 | func (b Nrf51822dk) CleanupRadio() error { 30 | return b.Micro.CleanupRadio() 31 | } 32 | 33 | func (b Nrf51822dk) Update(filePath string) error { 34 | b.Log.Info("Starting update") 35 | 36 | if err := b.Micro.ExtractFirmware(filePath, "nrf51422_xxac_s130.bin", "nrf51422_xxac_s130.dat"); err != nil { 37 | return err 38 | } 39 | 40 | name, err := bluetooth.GetName(b.Micro.LocalUUID) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | if name != "DfuTarg" { 46 | b.Log.Debug("Starting bootloader") 47 | 48 | client, err := bluetooth.Connect(b.Micro.LocalUUID) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if err = bluetooth.WriteDescriptor(client, dfu.CCCD, []byte{0x001}); err != nil { 54 | return err 55 | } 56 | 57 | // Ignore the error because this command causes the device to disconnect 58 | bluetooth.WriteCharacteristic(client, dfu, []byte{nrf51822.Start, 0x04}, false) 59 | 60 | // Give the device time to disconnect 61 | time.Sleep(shortTimeout) 62 | 63 | b.Log.Debug("Started bootloader") 64 | } else { 65 | b.Log.Debug("Bootloader already started") 66 | } 67 | 68 | client, err := bluetooth.Connect(b.Micro.LocalUUID) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | if err := b.Micro.Update(client); err != nil { 74 | return err 75 | } 76 | 77 | b.Log.Info("Finished update") 78 | 79 | return nil 80 | } 81 | 82 | func (b Nrf51822dk) Scan(applicationUUID int) (map[string]struct{}, error) { 83 | return bluetooth.Scan(strconv.Itoa(applicationUUID)) 84 | } 85 | 86 | func (b Nrf51822dk) Online() (bool, error) { 87 | return bluetooth.Online(b.Micro.LocalUUID) 88 | } 89 | 90 | func (b Nrf51822dk) Restart() error { 91 | b.Log.Info("Restarting...") 92 | return fmt.Errorf("Restart not implemented") 93 | } 94 | 95 | func (b Nrf51822dk) Identify() error { 96 | b.Log.Info("Identifying...") 97 | return fmt.Errorf("Identify not implemented") 98 | } 99 | 100 | func (b Nrf51822dk) UpdateConfig(config interface{}) error { 101 | b.Log.WithFields(log.Fields{ 102 | "Config": config, 103 | }).Info("Updating config...") 104 | return fmt.Errorf("Update config not implemented") 105 | } 106 | 107 | func (b Nrf51822dk) UpdateEnvironment(config interface{}) error { 108 | b.Log.WithFields(log.Fields{ 109 | "Config": config, 110 | }).Info("Updating environment...") 111 | return fmt.Errorf("Update environment not implemented") 112 | } 113 | 114 | func init() { 115 | log.SetLevel(config.GetLogLevel()) 116 | 117 | var err error 118 | if shortTimeout, err = config.GetShortBluetoothTimeout(); err != nil { 119 | log.WithFields(log.Fields{ 120 | "Error": err, 121 | }).Fatal("Unable to load bluetooth timeout") 122 | } 123 | 124 | dfu, err = bluetooth.GetCharacteristic("000015311212efde1523785feabcd123", ble.CharWrite+ble.CharNotify, 0x0F, 0x10) 125 | if err != nil { 126 | log.Fatal(err) 127 | } 128 | 129 | descriptor, err := bluetooth.GetDescriptor("2902", 0x11) 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | dfu.CCCD = descriptor 134 | 135 | log.Debug("Initialised nRF51822-DK characteristics") 136 | } 137 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "strconv" 7 | "time" 8 | 9 | log "github.com/Sirupsen/logrus" 10 | ) 11 | 12 | // GetLogLevel returns the log level 13 | func GetLogLevel() log.Level { 14 | return switchLogLevel(getEnv("ENM_LOG_LEVEL", "")) 15 | } 16 | 17 | // GetDependentLogLevel returns the log level for dependent devices 18 | func GetDependentLogLevel() log.Level { 19 | return switchLogLevel(getEnv("DEPENDENT_LOG_LEVEL", "")) 20 | } 21 | 22 | // GetSupervisorCheckDelay returns the time delay in seconds between each supervisor check at startup 23 | func GetSupervisorCheckDelay() (time.Duration, error) { 24 | value, err := strconv.Atoi(getEnv("ENM_SUPERVISOR_CHECK_DELAY", "1")) 25 | return time.Duration(value) * time.Second, err 26 | } 27 | 28 | // GetHotspotDeleteDelay returns the time delay in seconds between hotspot deletion and creation 29 | func GetHotspotDeleteDelay() (time.Duration, error) { 30 | value, err := strconv.Atoi(getEnv("ENM_HOTSPOT_DELETE_DELAY", "10")) 31 | return time.Duration(value) * time.Second, err 32 | } 33 | 34 | // GetLoopDelay returns the time delay in seconds between each application process loop 35 | func GetLoopDelay() (time.Duration, error) { 36 | value, err := strconv.Atoi(getEnv("ENM_CONFIG_LOOP_DELAY", "10")) 37 | return time.Duration(value) * time.Second, err 38 | } 39 | 40 | // GetPauseDelay returns the time delay in seconds between each pause check 41 | func GetPauseDelay() (time.Duration, error) { 42 | value, err := strconv.Atoi(getEnv("ENM_CONFIG_PAUSE_DELAY", "10")) 43 | return time.Duration(value) * time.Second, err 44 | } 45 | 46 | // GetHotspotSSID returns the SSID to be used for the hotspot 47 | func GetHotspotSSID() string { 48 | return getEnv("ENM_HOTSPOT_SSID", "resin-hotspot") 49 | } 50 | 51 | // GetHotspotPassword returns the password to be used for the hotspot 52 | func GetHotspotPassword() string { 53 | return getEnv("ENM_HOTSPOT_PASSWORD", "resin-hotspot") 54 | } 55 | 56 | // GetShortBluetoothTimeout returns the timeout for each instantaneous bluetooth operation 57 | func GetShortBluetoothTimeout() (time.Duration, error) { 58 | value, err := strconv.Atoi(getEnv("ENM_BLUETOOTH_SHORT_TIMEOUT", "1")) 59 | return time.Duration(value) * time.Second, err 60 | } 61 | 62 | // GetLongBluetoothTimeout returns the timeout for each long running bluetooth operation 63 | func GetLongBluetoothTimeout() (time.Duration, error) { 64 | value, err := strconv.Atoi(getEnv("ENM_BLUETOOTH_LONG_TIMEOUT", "10")) 65 | return time.Duration(value) * time.Second, err 66 | } 67 | 68 | // GetAvahiTimeout returns the timeout for each Avahi scan operation 69 | func GetAvahiTimeout() (time.Duration, error) { 70 | value, err := strconv.Atoi(getEnv("ENM_AVAHI_TIMEOUT", "10")) 71 | return time.Duration(value) * time.Second, err 72 | } 73 | 74 | // GetUpdateRetries returns the number of times the firmware update process should be attempted 75 | func GetUpdateRetries() (int, error) { 76 | return strconv.Atoi(getEnv("ENM_UPDATE_RETRIES", "1")) 77 | } 78 | 79 | // GetAssetsDir returns the root directory used to store the database and application commits 80 | func GetAssetsDir() string { 81 | return getEnv("ENM_ASSETS_DIRECTORY", "/data/assets") 82 | } 83 | 84 | // GetDbDir returns the directory used to store the database 85 | func GetDbDir() string { 86 | return getEnv("ENM_DB_DIRECTORY", "/data/database") 87 | } 88 | 89 | // GetDbPath returns the path used to store the database 90 | func GetDbPath() string { 91 | directory := GetDbDir() 92 | file := getEnv("ENM_DB_FILE", "enm.db") 93 | 94 | return path.Join(directory, file) 95 | } 96 | 97 | // GetVersion returns the API version used to communicate with the supervisor 98 | func GetVersion() string { 99 | return getEnv("ENM_API_VERSION", "v1") 100 | } 101 | 102 | // GetSuperAddr returns the address used to communicate with the supervisor 103 | func GetSuperAddr() string { 104 | return getEnv("RESIN_SUPERVISOR_ADDRESS", "http://127.0.0.1:4000") 105 | } 106 | 107 | // GetSuperAPIKey returns the API key used to communicate with the supervisor 108 | func GetSuperAPIKey() string { 109 | return getEnv("RESIN_SUPERVISOR_API_KEY", "") 110 | } 111 | 112 | // GetLockFileLocation returns the location of the lock file 113 | func GetLockFileLocation() string { 114 | return getEnv("ENM_LOCK_FILE_LOCATION", "/tmp/resin/resin-updates.lock") 115 | } 116 | 117 | func getEnv(key, fallback string) string { 118 | result := os.Getenv(key) 119 | if result == "" { 120 | result = fallback 121 | } 122 | return result 123 | } 124 | 125 | func switchLogLevel(level string) log.Level { 126 | switch level { 127 | case "Debug": 128 | return log.DebugLevel 129 | case "Info": 130 | return log.InfoLevel 131 | case "Warn": 132 | return log.WarnLevel 133 | case "Error": 134 | return log.ErrorLevel 135 | case "Fatal": 136 | return log.FatalLevel 137 | case "Panic": 138 | return log.PanicLevel 139 | } 140 | 141 | return log.InfoLevel 142 | } 143 | -------------------------------------------------------------------------------- /device/device.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/resin-io/edge-node-manager/board" 8 | "github.com/resin-io/edge-node-manager/board/esp8266" 9 | "github.com/resin-io/edge-node-manager/board/microbit" 10 | "github.com/resin-io/edge-node-manager/board/nrf51822dk" 11 | "github.com/resin-io/edge-node-manager/device/hook" 12 | "github.com/resin-io/edge-node-manager/device/status" 13 | "github.com/resin-io/edge-node-manager/micro/nrf51822" 14 | "github.com/resin-io/edge-node-manager/supervisor" 15 | ) 16 | 17 | type Device struct { 18 | Board board.Interface `json:"-"` 19 | ApplicationUUID int `storm:"index"` 20 | BoardType board.Type `storm:"index"` 21 | Name string `storm:"index"` 22 | LocalUUID string `storm:"index"` 23 | ResinUUID string `storm:"id,unique,index"` 24 | Commit string `storm:"index"` 25 | TargetCommit string `storm:"index"` 26 | Status status.Status `storm:"index"` 27 | Config map[string]interface{} `storm:"index"` 28 | TargetConfig map[string]interface{} `storm:"index"` 29 | Environment map[string]interface{} `storm:"index"` 30 | TargetEnvironment map[string]interface{} `storm:"index"` 31 | RestartFlag bool `storm:"index"` 32 | DeleteFlag bool `storm:"index"` 33 | } 34 | 35 | func (d Device) String() string { 36 | return fmt.Sprintf( 37 | "Application UUID: %d, "+ 38 | "Board type: %s, "+ 39 | "Name: %s, "+ 40 | "Local UUID: %s, "+ 41 | "Resin UUID: %s, "+ 42 | "Commit: %s, "+ 43 | "Target commit: %s, "+ 44 | "Status: %s, "+ 45 | "Config: %v, "+ 46 | "Target config: %v, "+ 47 | "Environment: %v, "+ 48 | "Target environment: %v, "+ 49 | "Restart: %t, "+ 50 | "Delete: %t", 51 | d.ApplicationUUID, 52 | d.BoardType, 53 | d.Name, 54 | d.LocalUUID, 55 | d.ResinUUID, 56 | d.Commit, 57 | d.TargetCommit, 58 | d.Status, 59 | d.Config, 60 | d.TargetConfig, 61 | d.Environment, 62 | d.TargetEnvironment, 63 | d.RestartFlag, 64 | d.DeleteFlag) 65 | } 66 | 67 | func New(applicationUUID int, boardType board.Type, name, localUUID, resinUUID string) Device { 68 | return Device{ 69 | ApplicationUUID: applicationUUID, 70 | BoardType: boardType, 71 | Name: name, 72 | LocalUUID: localUUID, 73 | ResinUUID: resinUUID, 74 | Commit: "", 75 | TargetCommit: "", 76 | Status: status.OFFLINE, 77 | } 78 | } 79 | 80 | func (d *Device) PopulateBoard() error { 81 | log := hook.Create(d.ResinUUID) 82 | 83 | switch d.BoardType { 84 | case board.MICROBIT: 85 | d.Board = microbit.Microbit{ 86 | Log: log, 87 | Micro: nrf51822.Nrf51822{ 88 | Log: log, 89 | LocalUUID: d.LocalUUID, 90 | Firmware: nrf51822.FIRMWARE{}, 91 | NotificationChannel: make(chan []byte), 92 | }, 93 | } 94 | case board.NRF51822DK: 95 | d.Board = nrf51822dk.Nrf51822dk{ 96 | Log: log, 97 | Micro: nrf51822.Nrf51822{ 98 | Log: log, 99 | LocalUUID: d.LocalUUID, 100 | Firmware: nrf51822.FIRMWARE{}, 101 | NotificationChannel: make(chan []byte), 102 | }, 103 | } 104 | case board.ESP8266: 105 | d.Board = esp8266.Esp8266{ 106 | Log: log, 107 | LocalUUID: d.LocalUUID, 108 | } 109 | default: 110 | return fmt.Errorf("Unsupported board type") 111 | } 112 | 113 | return nil 114 | } 115 | 116 | // Sync device with resin to ensure we have the latest values for: 117 | // - Device name 118 | // - Device target config 119 | // - Device target environment 120 | func (d *Device) Sync() []error { 121 | bytes, errs := supervisor.DependentDeviceInfo(d.ResinUUID) 122 | if errs != nil { 123 | return errs 124 | } 125 | 126 | var temp Device 127 | if err := json.Unmarshal(bytes, &temp); err != nil { 128 | // Ignore the error here as it means the device we are trying 129 | // to sync has been deleted 130 | return nil 131 | } 132 | 133 | d.Name = temp.Name 134 | d.TargetConfig = temp.TargetConfig 135 | d.TargetEnvironment = temp.TargetEnvironment 136 | 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /device/hook/hook.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "io/ioutil" 5 | "regexp" 6 | 7 | "github.com/Sirupsen/logrus" 8 | "github.com/resin-io/edge-node-manager/config" 9 | "github.com/resin-io/edge-node-manager/supervisor" 10 | ) 11 | 12 | type Hook struct { 13 | ResinUUID string 14 | } 15 | 16 | func (h *Hook) Fire(entry *logrus.Entry) error { 17 | serialised, _ := entry.Logger.Formatter.Format(entry) 18 | message := regexp.MustCompile(`\r?\n`).ReplaceAllString((string)(serialised), "") 19 | supervisor.DependentDeviceLog(h.ResinUUID, message) 20 | 21 | return nil 22 | } 23 | 24 | func (h *Hook) Levels() []logrus.Level { 25 | return []logrus.Level{ 26 | logrus.PanicLevel, 27 | logrus.FatalLevel, 28 | logrus.ErrorLevel, 29 | logrus.WarnLevel, 30 | logrus.InfoLevel, 31 | logrus.DebugLevel, 32 | } 33 | } 34 | 35 | func Create(resinUUID string) *logrus.Logger { 36 | log := logrus.New() 37 | log.Out = ioutil.Discard 38 | log.Level = config.GetDependentLogLevel() 39 | log.Formatter = &logrus.TextFormatter{ForceColors: true, DisableTimestamp: true} 40 | log.Hooks.Add(&Hook{ 41 | ResinUUID: resinUUID, 42 | }) 43 | 44 | return log 45 | } 46 | -------------------------------------------------------------------------------- /device/status/status.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | // Status defines the device statuses 4 | type Status string 5 | 6 | const ( 7 | DOWNLOADING Status = "Downloading" 8 | INSTALLING = "Installing" 9 | STARTING = "Starting" 10 | STOPPING = "Stopping" 11 | IDLE = "Idle" 12 | OFFLINE = "Offline" 13 | ) 14 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: fbc8dae34f332f4e4e74ba54b2786f9c7b1141657a9ed4f274456e0792a218b1 2 | updated: 2017-09-07T12:26:28.66483479+01:00 3 | imports: 4 | - name: github.com/asdine/storm 5 | version: 2da548c16156b3197728372bff5614033084aff5 6 | subpackages: 7 | - codec 8 | - codec/json 9 | - index 10 | - internal 11 | - q 12 | - name: github.com/boltdb/bolt 13 | version: e9cf4fae01b5a8ff89d0ec6b32f0d9c9f79aefdd 14 | - name: github.com/cavaliercoder/grab 15 | version: e403b038e10c3c6aaf022ab0f8502bd659604140 16 | - name: github.com/cenkalti/backoff 17 | version: 61153c768f31ee5f130071d08fc82b85208528de 18 | - name: github.com/currantlabs/ble 19 | version: 0ebf31a6e39ffff68e811e0f139689f1ef81addc 20 | subpackages: 21 | - linux 22 | - linux/adv 23 | - linux/att 24 | - linux/gatt 25 | - linux/hci 26 | - linux/hci/cmd 27 | - linux/hci/evt 28 | - linux/hci/socket 29 | - name: github.com/dsnet/compress 30 | version: b9aab3c6a04eef14c56384b4ad065e7b73438862 31 | subpackages: 32 | - bzip2 33 | - bzip2/internal/sais 34 | - internal 35 | - internal/prefix 36 | - name: github.com/fredli74/lockfile 37 | version: ad36330923e05763a7607f623b98c383bed727e2 38 | - name: github.com/godbus/dbus 39 | version: b038411ec341afd52c6d36d8baae92a29e6e8a9e 40 | - name: github.com/gorilla/context 41 | version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 42 | - name: github.com/gorilla/mux 43 | version: bcd8bc72b08df0f70df986b97f95590779502d31 44 | - name: github.com/grandcat/zeroconf 45 | version: 2f133dcabb8c1dc0e63ec6bb9e2c405179db3033 46 | - name: github.com/jmoiron/jsonq 47 | version: e874b168d07ecc7808bc950a17998a8aa3141d82 48 | - name: github.com/mattn/go-colorable 49 | version: a392f450ea64cee2b268dfaacdc2502b50a22b18 50 | - name: github.com/mattn/go-isatty 51 | version: 57fdcb988a5c543893cc61bce354a6e24ab70022 52 | - name: github.com/mgutz/ansi 53 | version: 9520e82c474b0a04dd04f8a40959027271bab992 54 | - name: github.com/mgutz/logxi 55 | version: aebf8a7d67ab4625e0fd4a665766fef9a709161b 56 | subpackages: 57 | - v1 58 | - name: github.com/mholt/archiver 59 | version: cdc68dd1f170b8dfc1a0d2231b5bb0967ed67006 60 | - name: github.com/miekg/dns 61 | version: e4205768578dc90c2669e75a2f8a8bf77e3083a4 62 | - name: github.com/moul/http2curl 63 | version: 4e24498b31dba4683efb9d35c1c8a91e2eda28c8 64 | - name: github.com/nwaples/rardecode 65 | version: f22b7ef81a0afac9ce1447d37e5ab8e99fbd2f73 66 | - name: github.com/parnurzeal/gorequest 67 | version: a578a48e8d6ca8b01a3b18314c43c6716bb5f5a3 68 | - name: github.com/pkg/errors 69 | version: 645ef00459ed84a119197bfb8d8205042c6df63d 70 | - name: github.com/Sirupsen/logrus 71 | version: ba1b36c82c5e05c4f912a88eab0dcd91a171688f 72 | - name: github.com/ulikunitz/xz 73 | version: 3807218c9f4ed05861fa9eb75b8fb8afd3325a34 74 | subpackages: 75 | - internal/hash 76 | - internal/xlog 77 | - lzma 78 | - name: github.com/verybluebot/tarinator-go 79 | version: f75724675c91d0c731b69c81e0985de07663f007 80 | - name: golang.org/x/net 81 | version: 090ebbdfc2aff44cc6674372b72e02e731f7f0ef 82 | subpackages: 83 | - bpf 84 | - context 85 | - internal/iana 86 | - internal/socket 87 | - ipv4 88 | - ipv6 89 | - publicsuffix 90 | - name: golang.org/x/sys 91 | version: d8f5ea21b9295e315e612b4bcf4bedea93454d4d 92 | subpackages: 93 | - unix 94 | testImports: [] 95 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/resin-io/edge-node-manager 2 | import: 3 | - package: github.com/Sirupsen/logrus 4 | version: ^0.11.5 5 | - package: github.com/asdine/storm 6 | version: ^0.8.0 7 | subpackages: 8 | - index 9 | - q 10 | - package: github.com/cavaliercoder/grab 11 | - package: github.com/currantlabs/ble 12 | subpackages: 13 | - linux 14 | - linux/hci 15 | - linux/hci/cmd 16 | - package: github.com/fredli74/lockfile 17 | - package: github.com/godbus/dbus 18 | version: b038411ec341afd52c6d36d8baae92a29e6e8a9e 19 | - package: github.com/gorilla/mux 20 | version: ^1.3.0 21 | - package: github.com/mholt/archiver 22 | version: ^2.0.0 23 | - package: github.com/parnurzeal/gorequest 24 | version: ^0.2.15 25 | - package: github.com/pkg/errors 26 | version: ^0.8.0 27 | - package: github.com/verybluebot/tarinator-go 28 | - package: golang.org/x/net 29 | subpackages: 30 | - context 31 | - package: github.com/jmoiron/jsonq 32 | - package: github.com/grandcat/zeroconf 33 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "sort" 8 | "time" 9 | 10 | log "github.com/Sirupsen/logrus" 11 | "github.com/asdine/storm" 12 | "github.com/jmoiron/jsonq" 13 | "github.com/resin-io/edge-node-manager/api" 14 | "github.com/resin-io/edge-node-manager/application" 15 | "github.com/resin-io/edge-node-manager/config" 16 | "github.com/resin-io/edge-node-manager/device" 17 | "github.com/resin-io/edge-node-manager/process" 18 | "github.com/resin-io/edge-node-manager/supervisor" 19 | ) 20 | 21 | var ( 22 | // This variable will be populated at build time with the current version tag 23 | version string 24 | // This variable defines the delay between each processing loop 25 | loopDelay time.Duration 26 | ) 27 | 28 | func main() { 29 | log.Info("Starting edge-node-manager") 30 | 31 | if err := checkVersion(); err != nil { 32 | log.Error("Unable to check if edge-node-manager is up to date") 33 | } 34 | 35 | supervisor.WaitUntilReady() 36 | 37 | for { 38 | // Run processing loop 39 | loop() 40 | 41 | // Delay between processing each set of applications to prevent 100% CPU usage 42 | time.Sleep(loopDelay) 43 | } 44 | } 45 | 46 | func init() { 47 | log.SetLevel(config.GetLogLevel()) 48 | log.SetFormatter(&log.TextFormatter{ForceColors: true, DisableTimestamp: true}) 49 | 50 | var err error 51 | loopDelay, err = config.GetLoopDelay() 52 | if err != nil { 53 | log.WithFields(log.Fields{ 54 | "Error": err, 55 | }).Fatal("Unable to load loop delay") 56 | } 57 | 58 | dbDir := config.GetDbDir() 59 | if err := os.MkdirAll(dbDir, os.ModePerm); err != nil { 60 | log.WithFields(log.Fields{ 61 | "Directory": dbDir, 62 | "Error": err, 63 | }).Fatal("Unable to create database directory") 64 | } 65 | 66 | db, err := storm.Open(config.GetDbPath()) 67 | if err != nil { 68 | log.WithFields(log.Fields{ 69 | "Error": err, 70 | }).Fatal("Unable to open database") 71 | } 72 | defer db.Close() 73 | 74 | if err := db.Init(&device.Device{}); err != nil { 75 | log.WithFields(log.Fields{ 76 | "Error": err, 77 | }).Fatal("Unable to initialise database") 78 | } 79 | 80 | go func() { 81 | router := api.NewRouter() 82 | port := ":1337" 83 | 84 | log.WithFields(log.Fields{ 85 | "Port": port, 86 | }).Debug("Initialising incoming supervisor API") 87 | 88 | if err := http.ListenAndServe(port, router); err != nil { 89 | log.WithFields(log.Fields{ 90 | "Error": err, 91 | }).Fatal("Unable to initialise incoming supervisor API") 92 | } 93 | }() 94 | } 95 | 96 | func checkVersion() error { 97 | resp, err := http.Get("https://api.github.com/repos/resin-io/edge-node-manager/releases/latest") 98 | if err != nil { 99 | return err 100 | } 101 | defer resp.Body.Close() 102 | 103 | data := map[string]interface{}{} 104 | if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { 105 | return err 106 | } 107 | 108 | latest, err := jsonq.NewQuery(data).String("tag_name") 109 | if err != nil { 110 | return err 111 | } 112 | 113 | if version == latest { 114 | log.WithFields(log.Fields{ 115 | "Current version": version, 116 | }).Info("edge-node-manager upto date") 117 | } else { 118 | log.WithFields(log.Fields{ 119 | "Current version": version, 120 | "Latest version": latest, 121 | "Update command": "git push resin master:resin-nocache", 122 | }).Warn("Please update edge-node-manager") 123 | } 124 | 125 | return nil 126 | } 127 | 128 | func loop() { 129 | // Get applications from the supervisor 130 | bytes, errs := supervisor.DependentApplicationsList() 131 | if errs != nil { 132 | log.WithFields(log.Fields{ 133 | "Errors": errs, 134 | }).Error("Unable to get applications") 135 | return 136 | } 137 | 138 | // Unmarshal applications 139 | applications, err := application.Unmarshal(bytes) 140 | if err != nil { 141 | log.WithFields(log.Fields{ 142 | "Error": err, 143 | }).Error("Unable to unmarshal applications") 144 | return 145 | } 146 | 147 | // Sort applications to ensure they run in order 148 | var keys []int 149 | for key := range applications { 150 | keys = append(keys, key) 151 | } 152 | sort.Ints(keys) 153 | 154 | // Process applications 155 | for _, key := range keys { 156 | if errs := process.Run(applications[key]); errs != nil { 157 | log.WithFields(log.Fields{ 158 | "Application": applications[key], 159 | "Errors": errs, 160 | }).Error("Unable to process application") 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /micro/nrf51822/nrf51822.go: -------------------------------------------------------------------------------- 1 | package nrf51822 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io/ioutil" 8 | "path" 9 | "time" 10 | 11 | log "github.com/Sirupsen/logrus" 12 | "github.com/currantlabs/ble" 13 | "github.com/mholt/archiver" 14 | "github.com/resin-io/edge-node-manager/config" 15 | "github.com/resin-io/edge-node-manager/radio/bluetooth" 16 | ) 17 | 18 | // Firmware-over-the-air update info 19 | // https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v11.0.0%2Fbledfu_transport_bleprofile.html 20 | // https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v11.0.0%2Fbledfu_transport_bleservice.html&anchor=ota_spec_control_state 21 | 22 | const ( 23 | Success byte = 0x01 24 | Start = 0x01 25 | Initialise = 0x02 26 | Receive = 0x03 27 | Validate = 0x04 28 | Activate = 0x05 29 | Restart = 0x06 30 | ReceivedSize = 0x07 31 | RequestBlockRecipt = 0x08 32 | Response = 0x10 33 | BlockRecipt = 0x11 34 | ) 35 | 36 | // Nrf51822 is a BLE SoC from Nordic 37 | // https://www.nordicsemi.com/eng/Products/Bluetooth-low-energy/nRF51822 38 | type Nrf51822 struct { 39 | Log *log.Logger 40 | LocalUUID string 41 | Firmware FIRMWARE 42 | NotificationChannel chan []byte 43 | } 44 | 45 | type FIRMWARE struct { 46 | currentBlock int 47 | size int 48 | binary []byte 49 | data []byte 50 | } 51 | 52 | var ( 53 | dfuPkt *ble.Characteristic 54 | dfuCtrl *ble.Characteristic 55 | shortTimeout time.Duration 56 | longTimeout time.Duration 57 | ) 58 | 59 | func (m *Nrf51822) InitialiseRadio() error { 60 | return bluetooth.Initialise() 61 | } 62 | 63 | func (m *Nrf51822) CleanupRadio() error { 64 | return bluetooth.Cleanup() 65 | } 66 | 67 | func (m *Nrf51822) ExtractFirmware(filePath, bin, data string) error { 68 | m.Log.WithFields(log.Fields{ 69 | "Firmware path": filePath, 70 | "Bin": bin, 71 | "Data": data, 72 | }).Debug("Extracting firmware") 73 | 74 | var err error 75 | 76 | if err = archiver.Zip.Open(path.Join(filePath, "application.zip"), filePath); err != nil { 77 | return err 78 | } 79 | 80 | m.Firmware.binary, err = ioutil.ReadFile(path.Join(filePath, bin)) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | m.Firmware.data, err = ioutil.ReadFile(path.Join(filePath, data)) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | m.Firmware.size = len(m.Firmware.binary) 91 | 92 | m.Log.WithFields(log.Fields{ 93 | "Size": m.Firmware.size, 94 | }).Debug("Extracted firmware") 95 | 96 | return nil 97 | } 98 | 99 | func (m *Nrf51822) Update(client ble.Client) error { 100 | if err := m.subscribe(client); err != nil { 101 | return err 102 | } 103 | defer client.ClearSubscriptions() 104 | 105 | if err := m.checkFOTA(client); err != nil { 106 | return err 107 | } 108 | 109 | if m.Firmware.currentBlock == 0 { 110 | if err := m.initFOTA(client); err != nil { 111 | return err 112 | } 113 | } 114 | 115 | if err := m.transferFOTA(client); err != nil { 116 | return err 117 | } 118 | 119 | if err := m.validateFOTA(client); err != nil { 120 | return err 121 | } 122 | 123 | return m.finaliseFOTA(client) 124 | } 125 | 126 | func init() { 127 | log.SetLevel(config.GetLogLevel()) 128 | 129 | var err error 130 | if shortTimeout, err = config.GetShortBluetoothTimeout(); err != nil { 131 | log.WithFields(log.Fields{ 132 | "Error": err, 133 | }).Fatal("Unable to load bluetooth timeout") 134 | } 135 | 136 | if longTimeout, err = config.GetLongBluetoothTimeout(); err != nil { 137 | log.WithFields(log.Fields{ 138 | "Error": err, 139 | }).Fatal("Unable to load bluetooth timeout") 140 | } 141 | 142 | dfuCtrl, err = bluetooth.GetCharacteristic("000015311212efde1523785feabcd123", ble.CharWrite+ble.CharNotify, 0x0F, 0x10) 143 | if err != nil { 144 | log.Fatal(err) 145 | } 146 | 147 | descriptor, err := bluetooth.GetDescriptor("2902", 0x11) 148 | if err != nil { 149 | log.Fatal(err) 150 | } 151 | dfuCtrl.CCCD = descriptor 152 | 153 | dfuPkt, err = bluetooth.GetCharacteristic("000015321212efde1523785feabcd123", ble.CharWriteNR, 0x0D, 0x0E) 154 | if err != nil { 155 | log.Fatal(err) 156 | } 157 | 158 | log.Debug("Initialised nRF51822 characteristics") 159 | } 160 | 161 | func (m *Nrf51822) subscribe(client ble.Client) error { 162 | if err := bluetooth.WriteDescriptor(client, dfuCtrl.CCCD, []byte{0x0001}); err != nil { 163 | return err 164 | } 165 | 166 | return client.Subscribe(dfuCtrl, false, func(b []byte) { 167 | m.NotificationChannel <- b 168 | }) 169 | } 170 | 171 | func (m *Nrf51822) checkFOTA(client ble.Client) error { 172 | m.Log.Debug("Checking FOTA") 173 | 174 | if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{ReceivedSize}, false); err != nil { 175 | return err 176 | } 177 | 178 | resp, err := m.getNotification([]byte{Response, ReceivedSize, Success}, true) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | m.Firmware.currentBlock, err = unpack(resp[3:]) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | m.Log.WithFields(log.Fields{ 189 | "Start block": m.Firmware.currentBlock, 190 | }).Debug("Checked FOTA") 191 | 192 | return nil 193 | } 194 | 195 | func (m *Nrf51822) initFOTA(client ble.Client) error { 196 | m.Log.Debug("Initialising FOTA") 197 | 198 | if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Start, 0x04}, false); err != nil { 199 | return err 200 | } 201 | 202 | buf := new(bytes.Buffer) 203 | if _, err := buf.Write(make([]byte, 8)); err != nil { 204 | return err 205 | } 206 | 207 | if err := binary.Write(buf, binary.LittleEndian, (int32)(m.Firmware.size)); err != nil { 208 | return err 209 | } 210 | 211 | if err := bluetooth.WriteCharacteristic(client, dfuPkt, buf.Bytes(), false); err != nil { 212 | return err 213 | } 214 | 215 | if _, err := m.getNotification([]byte{Response, Start, Success}, true); err != nil { 216 | return err 217 | } 218 | 219 | if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Initialise, 0x00}, false); err != nil { 220 | return err 221 | } 222 | 223 | if err := bluetooth.WriteCharacteristic(client, dfuPkt, m.Firmware.data, false); err != nil { 224 | return err 225 | } 226 | 227 | if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Initialise, 0x01}, false); err != nil { 228 | return err 229 | } 230 | 231 | if _, err := m.getNotification([]byte{Response, Initialise, Success}, true); err != nil { 232 | return err 233 | } 234 | 235 | if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{RequestBlockRecipt, 0x64, 0x00}, false); err != nil { 236 | return err 237 | } 238 | 239 | if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Receive}, false); err != nil { 240 | return err 241 | } 242 | 243 | m.Log.Debug("Initialised FOTA") 244 | 245 | return nil 246 | } 247 | 248 | func (m *Nrf51822) transferFOTA(client ble.Client) error { 249 | blockCounter := 1 250 | blockSize := 20 251 | 252 | if m.Firmware.currentBlock != 0 { 253 | blockCounter += (m.Firmware.currentBlock / blockSize) 254 | } 255 | 256 | m.Log.WithFields(log.Fields{ 257 | "Progress %": m.getProgress(), 258 | }).Info("Transferring FOTA") 259 | 260 | for i := m.Firmware.currentBlock; i < m.Firmware.size; i += blockSize { 261 | sliceIndex := i + blockSize 262 | if sliceIndex > m.Firmware.size { 263 | sliceIndex = m.Firmware.size 264 | } 265 | block := m.Firmware.binary[i:sliceIndex] 266 | 267 | if err := bluetooth.WriteCharacteristic(client, dfuPkt, block, true); err != nil { 268 | return err 269 | } 270 | 271 | if (blockCounter % 100) == 0 { 272 | resp, err := m.getNotification(nil, false) 273 | if err != nil { 274 | return err 275 | } 276 | 277 | if resp[0] != BlockRecipt { 278 | return fmt.Errorf("Incorrect notification received") 279 | } 280 | 281 | if m.Firmware.currentBlock, err = unpack(resp[1:]); err != nil { 282 | return err 283 | } 284 | 285 | if (i + blockSize) != m.Firmware.currentBlock { 286 | return fmt.Errorf("FOTA transer out of sync") 287 | } 288 | 289 | m.Log.WithFields(log.Fields{ 290 | "Progress %": m.getProgress(), 291 | }).Info("Transferring FOTA") 292 | } 293 | 294 | blockCounter++ 295 | } 296 | 297 | if _, err := m.getNotification([]byte{Response, Receive, Success}, true); err != nil { 298 | return err 299 | } 300 | 301 | m.Log.WithFields(log.Fields{ 302 | "Progress %": 100, 303 | }).Info("Transferring FOTA") 304 | 305 | return nil 306 | } 307 | 308 | func (m *Nrf51822) validateFOTA(client ble.Client) error { 309 | m.Log.Debug("Validating FOTA") 310 | 311 | if err := m.checkFOTA(client); err != nil { 312 | return err 313 | } 314 | 315 | if m.Firmware.currentBlock != m.Firmware.size { 316 | return fmt.Errorf("Bytes received does not match binary size") 317 | } 318 | 319 | if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Validate}, false); err != nil { 320 | return err 321 | } 322 | 323 | if _, err := m.getNotification([]byte{Response, Validate, Success}, true); err != nil { 324 | return err 325 | } 326 | 327 | m.Log.Debug("Validated FOTA") 328 | 329 | return nil 330 | } 331 | 332 | func (m Nrf51822) finaliseFOTA(client ble.Client) error { 333 | m.Log.Debug("Finalising FOTA") 334 | 335 | // Ignore the error because this command causes the device to disconnect 336 | bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Activate}, false) 337 | 338 | // Give the device time to disconnect 339 | time.Sleep(shortTimeout) 340 | 341 | m.Log.Debug("Finalised FOTA") 342 | 343 | return nil 344 | } 345 | 346 | func (m *Nrf51822) getNotification(exp []byte, compare bool) ([]byte, error) { 347 | select { 348 | case <-time.After(longTimeout): 349 | return nil, fmt.Errorf("Timed out waiting for notification") 350 | case resp := <-m.NotificationChannel: 351 | if !compare || bytes.Equal(resp[:3], exp) { 352 | return resp, nil 353 | } 354 | 355 | m.Log.WithFields(log.Fields{ 356 | "[0]": fmt.Sprintf("0x%X", resp[0]), 357 | "[1]": fmt.Sprintf("0x%X", resp[1]), 358 | "[2]": fmt.Sprintf("0x%X", resp[2]), 359 | }).Debug("Received") 360 | 361 | m.Log.WithFields(log.Fields{ 362 | "[0]": fmt.Sprintf("0x%X", exp[0]), 363 | "[1]": fmt.Sprintf("0x%X", exp[1]), 364 | "[2]": fmt.Sprintf("0x%X", exp[2]), 365 | }).Debug("Expected") 366 | 367 | return nil, fmt.Errorf("Incorrect notification received") 368 | } 369 | } 370 | 371 | func (m *Nrf51822) getProgress() float32 { 372 | return ((float32)(m.Firmware.currentBlock) / (float32)(m.Firmware.size)) * 100.0 373 | } 374 | 375 | func unpack(resp []byte) (int, error) { 376 | var result int32 377 | buf := bytes.NewReader(resp) 378 | if err := binary.Read(buf, binary.LittleEndian, &result); err != nil { 379 | return 0, err 380 | } 381 | return (int)(result), nil 382 | } 383 | -------------------------------------------------------------------------------- /process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "strconv" 7 | "time" 8 | 9 | log "github.com/Sirupsen/logrus" 10 | "github.com/asdine/storm" 11 | "github.com/asdine/storm/index" 12 | "github.com/asdine/storm/q" 13 | "github.com/fredli74/lockfile" 14 | "github.com/resin-io/edge-node-manager/application" 15 | "github.com/resin-io/edge-node-manager/config" 16 | "github.com/resin-io/edge-node-manager/device" 17 | deviceStatus "github.com/resin-io/edge-node-manager/device/status" 18 | processStatus "github.com/resin-io/edge-node-manager/process/status" 19 | "github.com/resin-io/edge-node-manager/supervisor" 20 | tarinator "github.com/verybluebot/tarinator-go" 21 | ) 22 | 23 | var ( 24 | CurrentStatus processStatus.Status 25 | TargetStatus processStatus.Status 26 | updateRetries int 27 | pauseDelay time.Duration 28 | lockLocation string 29 | lock *lockfile.LockFile 30 | ) 31 | 32 | func Run(a application.Application) []error { 33 | log.Info("----------------------------------------") 34 | 35 | // Pause the process if necessary 36 | if err := pause(); err != nil { 37 | return []error{err} 38 | } 39 | 40 | // Initialise the radio 41 | if err := a.Board.InitialiseRadio(); err != nil { 42 | return []error{err} 43 | } 44 | defer a.Board.CleanupRadio() 45 | 46 | if log.GetLevel() == log.DebugLevel { 47 | log.WithFields(log.Fields{ 48 | "Application": a, 49 | }).Debug("Processing application") 50 | } else { 51 | log.WithFields(log.Fields{ 52 | "Application": a.Name, 53 | }).Info("Processing application") 54 | } 55 | 56 | // Enable update locking 57 | var err error 58 | lock, err = lockfile.Lock(lockLocation) 59 | if err != nil { 60 | return []error{err} 61 | } 62 | defer lock.Unlock() 63 | 64 | // Handle delete flags 65 | if err := handleDelete(a); err != nil { 66 | return []error{err} 67 | } 68 | 69 | // Get all online devices associated with this application 70 | onlineDevices, err := getOnlineDevices(a) 71 | if err != nil { 72 | return []error{err} 73 | } 74 | 75 | // Get all provisioned devices associated with this application 76 | provisionedDevices, err := getProvisionedDevices(a) 77 | if err != nil { 78 | return []error{err} 79 | } 80 | 81 | if log.GetLevel() == log.DebugLevel { 82 | log.WithFields(log.Fields{ 83 | "Provisioned devices": provisionedDevices, 84 | }).Debug("Processing application") 85 | } else { 86 | log.WithFields(log.Fields{ 87 | "Number of provisioned devices": len(provisionedDevices), 88 | }).Info("Processing application") 89 | } 90 | 91 | // Convert provisioned devices to a hash map 92 | hashmap := make(map[string]struct{}) 93 | var s struct{} 94 | for _, value := range provisionedDevices { 95 | hashmap[value.LocalUUID] = s 96 | } 97 | 98 | // Provision all unprovisioned devices associated with this application 99 | for key := range onlineDevices { 100 | if _, ok := hashmap[key]; ok { 101 | // Device already provisioned 102 | continue 103 | } 104 | 105 | // Device not already provisioned 106 | if errs := provisionDevice(a, key); errs != nil { 107 | return errs 108 | } 109 | } 110 | 111 | // Refesh all provisioned devices associated with this application 112 | provisionedDevices, err = getProvisionedDevices(a) 113 | if err != nil { 114 | return []error{err} 115 | } 116 | 117 | // Sync all provisioned devices associated with this application 118 | for _, value := range provisionedDevices { 119 | if errs := value.Sync(); errs != nil { 120 | return errs 121 | } 122 | 123 | if err := updateDevice(value); err != nil { 124 | return []error{err} 125 | } 126 | } 127 | 128 | // Refesh all provisioned devices associated with this application 129 | provisionedDevices, err = getProvisionedDevices(a) 130 | if err != nil { 131 | return []error{err} 132 | } 133 | 134 | // Set state for all provisioned devices associated with this application 135 | for _, value := range provisionedDevices { 136 | if _, ok := onlineDevices[value.LocalUUID]; ok { 137 | value.Status = deviceStatus.IDLE 138 | } else { 139 | value.Status = deviceStatus.OFFLINE 140 | } 141 | 142 | if err := updateDevice(value); err != nil { 143 | return []error{err} 144 | } 145 | 146 | if errs := sendState(value); errs != nil { 147 | return errs 148 | } 149 | } 150 | 151 | // Refesh all provisioned devices associated with this application 152 | provisionedDevices, err = getProvisionedDevices(a) 153 | if err != nil { 154 | return []error{err} 155 | } 156 | 157 | // Update all online, outdated, provisioned devices associated with this application 158 | for _, value := range provisionedDevices { 159 | if (value.Commit != value.TargetCommit) && (value.Status != deviceStatus.OFFLINE) { 160 | // Populate board (and micro) for the device 161 | if err := value.PopulateBoard(); err != nil { 162 | return []error{err} 163 | } 164 | 165 | // Perform the update 166 | if errs := updateFirmware(value); errs != nil { 167 | return errs 168 | } 169 | } 170 | } 171 | 172 | return nil 173 | } 174 | 175 | func init() { 176 | log.SetLevel(config.GetLogLevel()) 177 | 178 | var err error 179 | if pauseDelay, err = config.GetPauseDelay(); err != nil { 180 | log.WithFields(log.Fields{ 181 | "Error": err, 182 | }).Fatal("Unable to load pause delay") 183 | } 184 | 185 | if updateRetries, err = config.GetUpdateRetries(); err != nil { 186 | log.WithFields(log.Fields{ 187 | "Error": err, 188 | }).Fatal("Unable to update retries") 189 | } 190 | 191 | lockLocation = config.GetLockFileLocation() 192 | 193 | CurrentStatus = processStatus.RUNNING 194 | TargetStatus = processStatus.RUNNING 195 | 196 | log.Debug("Initialised process") 197 | } 198 | 199 | func pause() error { 200 | if TargetStatus != processStatus.PAUSED { 201 | return nil 202 | } 203 | 204 | CurrentStatus = processStatus.PAUSED 205 | log.WithFields(log.Fields{ 206 | "Status": CurrentStatus, 207 | }).Info("Process status") 208 | 209 | for TargetStatus == processStatus.PAUSED { 210 | time.Sleep(pauseDelay) 211 | } 212 | 213 | CurrentStatus = processStatus.RUNNING 214 | log.WithFields(log.Fields{ 215 | "Status": CurrentStatus, 216 | }).Info("Process status") 217 | 218 | return nil 219 | } 220 | 221 | func getOnlineDevices(a application.Application) (map[string]struct{}, error) { 222 | onlineDevices, err := a.Board.Scan(a.ResinUUID) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | log.WithFields(log.Fields{ 228 | "Number of online devices": len(onlineDevices), 229 | }).Info("Processing application") 230 | 231 | return onlineDevices, nil 232 | } 233 | 234 | func getProvisionedDevices(a application.Application) ([]device.Device, error) { 235 | db, err := storm.Open(config.GetDbPath()) 236 | if err != nil { 237 | return nil, err 238 | } 239 | defer db.Close() 240 | 241 | var provisionedDevices []device.Device 242 | if err := db.Find("ApplicationUUID", a.ResinUUID, &provisionedDevices); err != nil && err.Error() != index.ErrNotFound.Error() { 243 | return nil, err 244 | } 245 | 246 | return provisionedDevices, nil 247 | } 248 | 249 | func provisionDevice(a application.Application, localUUID string) []error { 250 | log.WithFields(log.Fields{ 251 | "Local UUID": localUUID, 252 | }).Info("Provisioning device") 253 | 254 | resinUUID, name, errs := supervisor.DependentDeviceProvision(a.ResinUUID) 255 | if errs != nil { 256 | return errs 257 | } 258 | 259 | db, err := storm.Open(config.GetDbPath()) 260 | if err != nil { 261 | return []error{err} 262 | } 263 | defer db.Close() 264 | 265 | d := device.New(a.ResinUUID, a.BoardType, name, localUUID, resinUUID) 266 | if err := db.Save(&d); err != nil { 267 | return []error{err} 268 | } 269 | 270 | log.WithFields(log.Fields{ 271 | "Name": d.Name, 272 | "Local UUID": d.LocalUUID, 273 | }).Info("Provisioned device") 274 | 275 | return nil 276 | } 277 | 278 | func updateDevice(d device.Device) error { 279 | db, err := storm.Open(config.GetDbPath()) 280 | if err != nil { 281 | return err 282 | } 283 | defer db.Close() 284 | 285 | return db.Update(&d) 286 | } 287 | 288 | func sendState(d device.Device) []error { 289 | online := true 290 | if d.Status == deviceStatus.OFFLINE { 291 | online = false 292 | } 293 | 294 | return supervisor.DependentDeviceInfoUpdateWithOnlineState(d.ResinUUID, (string)(d.Status), d.Commit, online) 295 | } 296 | 297 | func updateFirmware(d device.Device) []error { 298 | online, err := d.Board.Online() 299 | if err != nil { 300 | return []error{err} 301 | } else if !online { 302 | return nil 303 | } 304 | 305 | filepath, err := getFirmware(d) 306 | if err != nil { 307 | return []error{err} 308 | } 309 | 310 | d.Status = deviceStatus.INSTALLING 311 | if err := updateDevice(d); err != nil { 312 | return []error{err} 313 | } 314 | if errs := sendState(d); errs != nil { 315 | return errs 316 | } 317 | 318 | for i := 1; i <= updateRetries; i++ { 319 | log.WithFields(log.Fields{ 320 | "Name": d.Name, 321 | "Attempt": i, 322 | }).Info("Starting update") 323 | 324 | if err := d.Board.Update(filepath); err != nil { 325 | log.WithFields(log.Fields{ 326 | "Name": d.Name, 327 | "Error": err, 328 | }).Error("Update failed") 329 | continue 330 | } else { 331 | log.WithFields(log.Fields{ 332 | "Name": d.Name, 333 | }).Info("Finished update") 334 | d.Commit = d.TargetCommit 335 | break 336 | } 337 | } 338 | 339 | d.Status = deviceStatus.IDLE 340 | if err := updateDevice(d); err != nil { 341 | return []error{err} 342 | } 343 | return sendState(d) 344 | } 345 | 346 | func getFirmware(d device.Device) (string, error) { 347 | // Build the file paths 348 | filepath := config.GetAssetsDir() 349 | filepath = path.Join(filepath, strconv.Itoa(d.ApplicationUUID)) 350 | filepath = path.Join(filepath, d.TargetCommit) 351 | tarPath := path.Join(filepath, "binary.tar") 352 | 353 | // Check if the firmware exists 354 | if _, err := os.Stat(tarPath); os.IsNotExist(err) { 355 | // Download the firmware 356 | if err := supervisor.DependentApplicationUpdate(d.ApplicationUUID, d.TargetCommit); err != nil { 357 | return "", err 358 | } 359 | 360 | // Extract the firmware 361 | if err := tarinator.UnTarinate(filepath, tarPath); err != nil { 362 | return "", err 363 | } 364 | } 365 | 366 | return filepath, nil 367 | } 368 | 369 | func handleDelete(a application.Application) error { 370 | db, err := storm.Open(config.GetDbPath()) 371 | if err != nil { 372 | return err 373 | } 374 | defer db.Close() 375 | 376 | if err := db.Select(q.Eq("ApplicationUUID", a.ResinUUID), q.Eq("DeleteFlag", true)).Delete(&device.Device{}); err != nil && err.Error() != index.ErrNotFound.Error() { 377 | return err 378 | } 379 | 380 | return nil 381 | } 382 | -------------------------------------------------------------------------------- /process/status/status.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | // Status defines the process statuses 4 | type Status string 5 | 6 | const ( 7 | RUNNING Status = "Running" 8 | PAUSED = "Paused" 9 | ) 10 | -------------------------------------------------------------------------------- /radio/bluetooth/bluetooth.go: -------------------------------------------------------------------------------- 1 | package bluetooth 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | "golang.org/x/net/context" 12 | 13 | log "github.com/Sirupsen/logrus" 14 | "github.com/currantlabs/ble" 15 | "github.com/currantlabs/ble/linux" 16 | "github.com/currantlabs/ble/linux/hci" 17 | "github.com/currantlabs/ble/linux/hci/cmd" 18 | "github.com/resin-io/edge-node-manager/config" 19 | ) 20 | 21 | var ( 22 | initialised bool 23 | doneChannel chan struct{} 24 | name *ble.Characteristic 25 | shortTimeout time.Duration 26 | longTimeout time.Duration 27 | ) 28 | 29 | func Initialise() error { 30 | if !initialised && os.Getenv("RESIN_DEVICE_TYPE") == "raspberrypi3" { 31 | log.Info("Initialising bluetooth") 32 | 33 | for i := 1; i <= 3; i++ { 34 | if err := exec.Command("bash", "-c", "/usr/bin/hciattach /dev/ttyAMA0 bcm43xx 921600 noflow -").Run(); err == nil { 35 | if err := exec.Command("bash", "-c", "hciconfig hci0 up").Run(); err != nil { 36 | return err 37 | } 38 | 39 | log.Info("Initialised bluetooth") 40 | 41 | initialised = true 42 | break 43 | } 44 | } 45 | 46 | // Small sleep to give the bluetooth interface time to settle 47 | time.Sleep(shortTimeout) 48 | } 49 | 50 | device, err := linux.NewDevice() 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if err := updateLinuxParam(device); err != nil { 56 | return err 57 | } 58 | 59 | ble.SetDefaultDevice(device) 60 | 61 | return nil 62 | } 63 | 64 | func Cleanup() error { 65 | return ble.Stop() 66 | } 67 | 68 | func Connect(id string) (ble.Client, error) { 69 | client, err := ble.Dial(ble.WithSigHandler(context.WithTimeout(context.Background(), longTimeout)), hci.RandomAddress{ble.NewAddr(id)}) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | if _, err := client.ExchangeMTU(ble.MaxMTU); err != nil { 75 | return nil, err 76 | } 77 | 78 | doneChannel = make(chan struct{}) 79 | go func() { 80 | <-client.Disconnected() 81 | close(doneChannel) 82 | }() 83 | 84 | return client, nil 85 | } 86 | 87 | func Disconnect(client ble.Client) error { 88 | if err := client.ClearSubscriptions(); err != nil { 89 | return err 90 | } 91 | 92 | if err := client.CancelConnection(); err != nil { 93 | return err 94 | } 95 | <-doneChannel 96 | 97 | return nil 98 | } 99 | 100 | func WriteCharacteristic(client ble.Client, characteristic *ble.Characteristic, value []byte, noRsp bool) error { 101 | err := make(chan error) 102 | go func() { 103 | err <- client.WriteCharacteristic(characteristic, value, noRsp) 104 | }() 105 | 106 | select { 107 | case done := <-err: 108 | return done 109 | case <-time.After(shortTimeout): 110 | return fmt.Errorf("Write characteristic timed out") 111 | } 112 | } 113 | 114 | func ReadCharacteristic(client ble.Client, characteristic *ble.Characteristic) ([]byte, error) { 115 | type Result struct { 116 | Val []byte 117 | Err error 118 | } 119 | 120 | result := make(chan Result) 121 | go func() { 122 | result <- func() Result { 123 | val, err := client.ReadCharacteristic(characteristic) 124 | return Result{val, err} 125 | }() 126 | }() 127 | 128 | select { 129 | case done := <-result: 130 | return done.Val, done.Err 131 | case <-time.After(shortTimeout): 132 | return nil, fmt.Errorf("Read characteristic timed out") 133 | } 134 | } 135 | 136 | func WriteDescriptor(client ble.Client, descriptor *ble.Descriptor, value []byte) error { 137 | err := make(chan error) 138 | go func() { 139 | err <- client.WriteDescriptor(descriptor, value) 140 | }() 141 | 142 | select { 143 | case done := <-err: 144 | return done 145 | case <-time.After(shortTimeout): 146 | return fmt.Errorf("Write descriptor timed out") 147 | } 148 | } 149 | 150 | func Scan(id string) (map[string]struct{}, error) { 151 | devices := make(map[string]struct{}) 152 | advChannel := make(chan ble.Advertisement) 153 | ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), longTimeout)) 154 | 155 | go func() { 156 | for { 157 | select { 158 | case <-ctx.Done(): 159 | return 160 | case adv := <-advChannel: 161 | if strings.EqualFold(adv.LocalName(), id) { 162 | var s struct{} 163 | devices[adv.Address().String()] = s 164 | } 165 | } 166 | } 167 | }() 168 | 169 | err := ble.Scan(ctx, false, func(adv ble.Advertisement) { advChannel <- adv }, nil) 170 | if errors.Cause(err) != context.DeadlineExceeded && errors.Cause(err) != context.Canceled { 171 | return devices, err 172 | } 173 | 174 | return devices, nil 175 | } 176 | 177 | func Online(id string) (bool, error) { 178 | online := false 179 | advChannel := make(chan ble.Advertisement) 180 | ctx, cancel := context.WithCancel(context.Background()) 181 | ctx = ble.WithSigHandler(context.WithTimeout(ctx, longTimeout)) 182 | 183 | go func() { 184 | for { 185 | select { 186 | case <-ctx.Done(): 187 | return 188 | case adv := <-advChannel: 189 | if strings.EqualFold(adv.Address().String(), id) { 190 | online = true 191 | cancel() 192 | } 193 | } 194 | } 195 | }() 196 | 197 | err := ble.Scan(ctx, false, func(adv ble.Advertisement) { advChannel <- adv }, nil) 198 | if errors.Cause(err) != context.DeadlineExceeded && errors.Cause(err) != context.Canceled { 199 | return online, err 200 | } 201 | 202 | return online, nil 203 | } 204 | 205 | func GetName(id string) (string, error) { 206 | client, err := Connect(id) 207 | if err != nil { 208 | return "", err 209 | } 210 | 211 | resp, err := ReadCharacteristic(client, name) 212 | if err != nil { 213 | return "", err 214 | } 215 | 216 | if err := Disconnect(client); err != nil { 217 | return "", err 218 | } 219 | 220 | return string(resp), nil 221 | } 222 | 223 | func GetCharacteristic(uuid string, property ble.Property, handle, vhandle uint16) (*ble.Characteristic, error) { 224 | parsedUUID, err := ble.Parse(uuid) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | characteristic := ble.NewCharacteristic(parsedUUID) 230 | characteristic.Property = property 231 | characteristic.Handle = handle 232 | characteristic.ValueHandle = vhandle 233 | 234 | return characteristic, nil 235 | } 236 | 237 | func GetDescriptor(uuid string, handle uint16) (*ble.Descriptor, error) { 238 | parsedUUID, err := ble.Parse(uuid) 239 | if err != nil { 240 | return nil, err 241 | } 242 | 243 | descriptor := ble.NewDescriptor(parsedUUID) 244 | descriptor.Handle = handle 245 | 246 | return descriptor, nil 247 | } 248 | 249 | func init() { 250 | log.SetLevel(config.GetLogLevel()) 251 | 252 | var err error 253 | if shortTimeout, err = config.GetShortBluetoothTimeout(); err != nil { 254 | log.WithFields(log.Fields{ 255 | "Error": err, 256 | }).Fatal("Unable to load bluetooth timeout") 257 | } 258 | 259 | if longTimeout, err = config.GetLongBluetoothTimeout(); err != nil { 260 | log.WithFields(log.Fields{ 261 | "Error": err, 262 | }).Fatal("Unable to load bluetooth timeout") 263 | } 264 | 265 | name, err = GetCharacteristic("2a00", ble.CharRead+ble.CharWrite, 0x02, 0x03) 266 | if err != nil { 267 | log.Fatal(err) 268 | } 269 | 270 | log.Debug("Initialised bluetooth radio") 271 | } 272 | 273 | func updateLinuxParam(device *linux.Device) error { 274 | if err := device.HCI.Send(&cmd.LESetScanParameters{ 275 | LEScanType: 0x00, // 0x00: passive, 0x01: active 276 | LEScanInterval: 0x0060, // 0x0004 - 0x4000; N * 0.625msec 277 | LEScanWindow: 0x0060, // 0x0004 - 0x4000; N * 0.625msec 278 | OwnAddressType: 0x01, // 0x00: public, 0x01: random 279 | ScanningFilterPolicy: 0x00, // 0x00: accept all, 0x01: ignore non-white-listed. 280 | }, nil); err != nil { 281 | return errors.Wrap(err, "can't set scan param") 282 | } 283 | 284 | if err := device.HCI.Option(hci.OptConnParams( 285 | cmd.LECreateConnection{ 286 | LEScanInterval: 0x0060, // 0x0004 - 0x4000; N * 0.625 msec 287 | LEScanWindow: 0x0060, // 0x0004 - 0x4000; N * 0.625 msec 288 | InitiatorFilterPolicy: 0x00, // White list is not used 289 | PeerAddressType: 0x00, // Public Device Address 290 | PeerAddress: [6]byte{}, // 291 | OwnAddressType: 0x00, // Public Device Address 292 | ConnIntervalMin: 0x0028, // 0x0006 - 0x0C80; N * 1.25 msec 293 | ConnIntervalMax: 0x0038, // 0x0006 - 0x0C80; N * 1.25 msec 294 | ConnLatency: 0x0000, // 0x0000 - 0x01F3; N * 1.25 msec 295 | SupervisionTimeout: 0x002A, // 0x000A - 0x0C80; N * 10 msec 296 | MinimumCELength: 0x0000, // 0x0000 - 0xFFFF; N * 0.625 msec 297 | MaximumCELength: 0x0000, // 0x0000 - 0xFFFF; N * 0.625 msec 298 | })); err != nil { 299 | return errors.Wrap(err, "can't set connection param") 300 | } 301 | return nil 302 | } 303 | -------------------------------------------------------------------------------- /radio/wifi/nm.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/godbus/dbus" 7 | ) 8 | 9 | type NmDeviceState uint32 10 | 11 | const ( 12 | NmDeviceStateUnknown NmDeviceState = 0 13 | NmDeviceStateUnmanaged NmDeviceState = 10 14 | NmDeviceStateUnavailable NmDeviceState = 20 15 | NmDeviceStateDisconnected NmDeviceState = 30 16 | NmDeviceStatePrepare NmDeviceState = 40 17 | NmDeviceStateConfig NmDeviceState = 50 18 | NmDeviceStateNeed_auth NmDeviceState = 60 19 | NmDeviceStateIp_config NmDeviceState = 70 20 | NmDeviceStateIp_check NmDeviceState = 80 21 | NmDeviceStateSecondaries NmDeviceState = 90 22 | NmDeviceStateActivated NmDeviceState = 100 23 | NmDeviceStateDeactivating NmDeviceState = 110 24 | NmDeviceStateFailed NmDeviceState = 120 25 | ) 26 | 27 | type NmDeviceType uint32 28 | 29 | const ( 30 | NmDeviceTypeUnknown NmDeviceType = 0 31 | NmDeviceTypeEthernet NmDeviceType = 1 32 | NmDeviceTypeWifi NmDeviceType = 2 33 | NmDeviceTypeUnused1 NmDeviceType = 3 34 | NmDeviceTypeUnused2 NmDeviceType = 4 35 | NmDeviceTypeBt NmDeviceType = 5 36 | NmDeviceTypeOlpcMesh NmDeviceType = 6 37 | NmDeviceTypeWimax NmDeviceType = 7 38 | NmDeviceTypeModem NmDeviceType = 8 39 | NmDeviceTypeInfiniband NmDeviceType = 9 40 | NmDeviceTypeBond NmDeviceType = 10 41 | NmDeviceTypeVlan NmDeviceType = 11 42 | NmDeviceTypeAdsl NmDeviceType = 12 43 | NmDeviceTypeBridge NmDeviceType = 13 44 | NmDeviceTypeGeneric NmDeviceType = 14 45 | NmDeviceTypeTeam NmDeviceType = 15 46 | ) 47 | 48 | type NmActiveConnectionState uint32 49 | 50 | const ( 51 | NmActiveConnectionStateUnknown NmActiveConnectionState = 0 52 | NmActiveConnectionStateActivating NmActiveConnectionState = 1 53 | NmActiveConnectionStateActivated NmActiveConnectionState = 2 54 | NmActiveConnectionStateDeactivating NmActiveConnectionState = 3 55 | NmActiveConnectionStateDeactivated NmActiveConnectionState = 4 56 | ) 57 | 58 | type NmDevice struct { 59 | nmPath dbus.ObjectPath 60 | nmState NmDeviceState 61 | nmType NmDeviceType 62 | nmInterface string 63 | } 64 | 65 | func removeHotspotConnections(ssid string) error { 66 | settingsObject, err := getConnection(ssid) 67 | if err != nil { 68 | return err 69 | } else if settingsObject == nil { 70 | return nil 71 | } 72 | 73 | if err := settingsObject.Call("org.freedesktop.NetworkManager.Settings.Connection.Delete", 0).Store(); err != nil { 74 | return err 75 | } 76 | 77 | for { 78 | if settingsObject, err := getConnection(ssid); err != nil { 79 | return err 80 | } else if settingsObject == nil { 81 | break 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func getConnection(ssid string) (dbus.BusObject, error) { 89 | connection, err := dbus.SystemBus() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | var settingsPaths []dbus.ObjectPath 95 | settingsObject := connection.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings") 96 | if err := settingsObject.Call("org.freedesktop.NetworkManager.Settings.ListConnections", 0).Store(&settingsPaths); err != nil { 97 | return nil, err 98 | } 99 | 100 | for _, settingsPath := range settingsPaths { 101 | var settings map[string]map[string]dbus.Variant 102 | settingsObject := connection.Object("org.freedesktop.NetworkManager", settingsPath) 103 | if err := settingsObject.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&settings); err != nil { 104 | return nil, err 105 | } 106 | 107 | if settings["connection"]["id"].Value().(string) == ssid { 108 | return settingsObject, nil 109 | } 110 | } 111 | 112 | return nil, nil 113 | } 114 | 115 | func isEthernetConnected() (bool, error) { 116 | devices, err := getDevices() 117 | if err != nil { 118 | return false, err 119 | } 120 | 121 | for _, device := range devices { 122 | if device.nmType == NmDeviceTypeEthernet && device.nmState == NmDeviceStateActivated { 123 | return true, nil 124 | } 125 | } 126 | 127 | return false, nil 128 | } 129 | 130 | func getWifiDevice() (NmDevice, error) { 131 | devices, err := getDevices() 132 | if err != nil { 133 | return NmDevice{}, err 134 | } 135 | 136 | for _, device := range devices { 137 | if device.nmType == NmDeviceTypeWifi { 138 | return device, nil 139 | } 140 | } 141 | 142 | return NmDevice{}, fmt.Errorf("No wifi device found") 143 | } 144 | 145 | func getFreeWifiDevice() (NmDevice, error) { 146 | devices, err := getDevices() 147 | if err != nil { 148 | return NmDevice{}, err 149 | } 150 | 151 | for _, device := range devices { 152 | if device.nmType == NmDeviceTypeWifi && device.nmState == NmDeviceStateDisconnected { 153 | return device, nil 154 | } 155 | } 156 | 157 | return NmDevice{}, fmt.Errorf("No free wifi device found") 158 | } 159 | 160 | func createHotspotConnection(device NmDevice, ssid, password string) error { 161 | connection, err := dbus.SystemBus() 162 | if err != nil { 163 | return err 164 | } 165 | 166 | hotspot := make(map[string]map[string]interface{}) 167 | 168 | hotspot["802-11-wireless"] = make(map[string]interface{}) 169 | hotspot["802-11-wireless"]["band"] = "bg" 170 | hotspot["802-11-wireless"]["hidden"] = false 171 | hotspot["802-11-wireless"]["mode"] = "ap" 172 | hotspot["802-11-wireless"]["security"] = "802-11-wireless-security" 173 | hotspot["802-11-wireless"]["ssid"] = []byte(ssid) 174 | 175 | hotspot["802-11-wireless-security"] = make(map[string]interface{}) 176 | hotspot["802-11-wireless-security"]["key-mgmt"] = "wpa-psk" 177 | hotspot["802-11-wireless-security"]["psk"] = password 178 | 179 | hotspot["connection"] = make(map[string]interface{}) 180 | hotspot["connection"]["autoconnect"] = false 181 | hotspot["connection"]["id"] = ssid 182 | hotspot["connection"]["interface-name"] = device.nmInterface 183 | hotspot["connection"]["type"] = "801-11-wireless" 184 | 185 | hotspot["ipv4"] = make(map[string]interface{}) 186 | hotspot["ipv4"]["method"] = "shared" 187 | 188 | var path, activeConnectionPath dbus.ObjectPath 189 | rootObject := connection.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") 190 | if err := rootObject.Call( 191 | "org.freedesktop.NetworkManager.AddAndActivateConnection", 192 | 0, 193 | hotspot, 194 | device.nmPath, 195 | dbus.ObjectPath("/")). 196 | Store(&path, &activeConnectionPath); err != nil { 197 | return err 198 | } 199 | 200 | activeConnectionObject := connection.Object("org.freedesktop.NetworkManager", activeConnectionPath) 201 | for { 202 | value, err := getProperty(activeConnectionObject, "org.freedesktop.NetworkManager.Connection.Active.State") 203 | if err != nil { 204 | return err 205 | } 206 | 207 | if NmActiveConnectionState(value.(uint32)) == NmActiveConnectionStateActivated { 208 | break 209 | } 210 | } 211 | 212 | return nil 213 | } 214 | 215 | func getDevices() ([]NmDevice, error) { 216 | connection, err := dbus.SystemBus() 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | var paths []dbus.ObjectPath 222 | rootObject := connection.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") 223 | if err := rootObject.Call("org.freedesktop.NetworkManager.GetAllDevices", 0).Store(&paths); err != nil { 224 | return nil, err 225 | } 226 | 227 | devices := make([]NmDevice, 5) 228 | for _, path := range paths { 229 | deviceObject := connection.Object("org.freedesktop.NetworkManager", path) 230 | 231 | device := NmDevice{} 232 | device.nmPath = path 233 | 234 | value, err := getProperty(deviceObject, "org.freedesktop.NetworkManager.Device.State") 235 | if err != nil { 236 | return nil, err 237 | } 238 | device.nmState = NmDeviceState(value.(uint32)) 239 | 240 | value, err = getProperty(deviceObject, "org.freedesktop.NetworkManager.Device.DeviceType") 241 | if err != nil { 242 | return nil, err 243 | } 244 | device.nmType = NmDeviceType(value.(uint32)) 245 | 246 | value, err = getProperty(deviceObject, "org.freedesktop.NetworkManager.Device.Interface") 247 | if err != nil { 248 | return nil, err 249 | } 250 | device.nmInterface = value.(string) 251 | 252 | devices = append(devices, device) 253 | } 254 | 255 | return devices, nil 256 | } 257 | 258 | func getProperty(object dbus.BusObject, property string) (interface{}, error) { 259 | value, err := object.GetProperty(property) 260 | if err != nil { 261 | return nil, err 262 | } 263 | 264 | return value.Value(), nil 265 | } 266 | -------------------------------------------------------------------------------- /radio/wifi/wifi.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | log "github.com/Sirupsen/logrus" 12 | "github.com/grandcat/zeroconf" 13 | "github.com/parnurzeal/gorequest" 14 | "github.com/resin-io/edge-node-manager/config" 15 | ) 16 | 17 | var ( 18 | initialised bool 19 | avahiTimeout time.Duration 20 | ) 21 | 22 | type Host struct { 23 | ip string 24 | deviceType string 25 | applicationUUID string 26 | id string 27 | } 28 | 29 | func Initialise() error { 30 | if initialised { 31 | return nil 32 | } 33 | 34 | log.Info("Initialising wifi hotspot") 35 | 36 | os.Setenv("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/host/run/dbus/system_bus_socket") 37 | 38 | ssid := config.GetHotspotSSID() 39 | password := config.GetHotspotPassword() 40 | 41 | if err := removeHotspotConnections(ssid); err != nil { 42 | return err 43 | } 44 | 45 | if delay, err := config.GetHotspotDeleteDelay(); err != nil { 46 | return err 47 | } else { 48 | time.Sleep(delay) 49 | } 50 | 51 | // If ethernet is connected, create the hotspot on the first wifi interface found 52 | // If ethernet is not connected, create the hotspot on the first FREE wifi interface found 53 | var device NmDevice 54 | if ethernet, err := isEthernetConnected(); err != nil { 55 | return err 56 | } else if ethernet { 57 | if device, err = getWifiDevice(); err != nil { 58 | return err 59 | } 60 | } else { 61 | if device, err = getFreeWifiDevice(); err != nil { 62 | return err 63 | } 64 | } 65 | 66 | if err := createHotspotConnection(device, ssid, password); err != nil { 67 | return err 68 | } 69 | 70 | log.WithFields(log.Fields{ 71 | "SSID": ssid, 72 | "Password": password, 73 | "Device": device, 74 | }).Info("Initialised wifi hotspot") 75 | 76 | initialised = true 77 | return nil 78 | } 79 | 80 | func Cleanup() error { 81 | // Return as we do not want to disable the hotspot 82 | return nil 83 | } 84 | 85 | func Scan(id string) (map[string]struct{}, error) { 86 | hosts, err := scan() 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | online := make(map[string]struct{}) 92 | for _, host := range hosts { 93 | if host.applicationUUID == id { 94 | var s struct{} 95 | online[host.id] = s 96 | } 97 | } 98 | 99 | return online, nil 100 | } 101 | 102 | func Online(id string) (bool, error) { 103 | hosts, err := scan() 104 | if err != nil { 105 | return false, err 106 | } 107 | 108 | for _, host := range hosts { 109 | if host.id == id { 110 | return true, nil 111 | } 112 | } 113 | 114 | return false, nil 115 | } 116 | 117 | func GetIP(id string) (string, error) { 118 | hosts, err := scan() 119 | if err != nil { 120 | return "", err 121 | } 122 | 123 | for _, host := range hosts { 124 | if host.id == id { 125 | return host.ip, nil 126 | } 127 | } 128 | 129 | return "", fmt.Errorf("Device offline") 130 | } 131 | 132 | func PostForm(url, filePath string) error { 133 | req := gorequest.New() 134 | req.Post(url) 135 | req.Type("multipart") 136 | req.SendFile(filePath, "firmware.bin", "image") 137 | 138 | log.WithFields(log.Fields{ 139 | "URL": req.Url, 140 | "Method": req.Method, 141 | }).Info("Posting form") 142 | 143 | resp, _, errs := req.End() 144 | return handleResp(resp, errs, http.StatusOK) 145 | } 146 | 147 | func init() { 148 | log.SetLevel(config.GetLogLevel()) 149 | 150 | var err error 151 | if avahiTimeout, err = config.GetAvahiTimeout(); err != nil { 152 | log.WithFields(log.Fields{ 153 | "Error": err, 154 | }).Fatal("Unable to load Avahi timeout") 155 | } 156 | 157 | log.Debug("Initialised wifi") 158 | } 159 | 160 | func scan() ([]Host, error) { 161 | ctx, cancel := context.WithTimeout(context.Background(), avahiTimeout) 162 | defer cancel() 163 | 164 | resolver, err := zeroconf.NewResolver(nil) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | entries := make(chan *zeroconf.ServiceEntry) 170 | var hosts []Host 171 | go func(entries <-chan *zeroconf.ServiceEntry, hosts *[]Host) { 172 | for entry := range entries { 173 | parts := strings.Split(entry.ServiceRecord.Instance, "_") 174 | 175 | if len(entry.AddrIPv4) < 1 || len(parts) < 3 { 176 | continue 177 | } 178 | 179 | host := Host{ 180 | ip: entry.AddrIPv4[0].String(), 181 | deviceType: parts[0], 182 | applicationUUID: parts[1], 183 | id: parts[2], 184 | } 185 | *hosts = append(*hosts, host) 186 | } 187 | }(entries, &hosts) 188 | 189 | err = resolver.Browse(ctx, "_http._tcp", "local", entries) 190 | if err != nil { 191 | log.WithFields(log.Fields{ 192 | "Error": err, 193 | }).Error("Unable to scan") 194 | return nil, err 195 | } 196 | 197 | <-ctx.Done() 198 | 199 | return hosts, nil 200 | } 201 | 202 | func handleResp(resp gorequest.Response, errs []error, statusCode int) error { 203 | if errs != nil { 204 | return errs[0] 205 | } 206 | 207 | if resp.StatusCode != statusCode { 208 | return fmt.Errorf("Invalid response received: %s", resp.Status) 209 | } 210 | 211 | log.WithFields(log.Fields{ 212 | "Response": resp.Status, 213 | }).Debug("Valid response received") 214 | 215 | return nil 216 | } 217 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | 5 | if [ -z "$ACCOUNT" ] || [ -z "$REPO" ] || [ -z "$ACCESS_TOKEN" ] || [ -z "$TRAVIS_TAG" ]; then 6 | echo "Please set value for ACCOUNT, REPO, ACCESS_TOKEN and TRAVIS_TAG" 7 | exit 1 8 | fi 9 | 10 | echo "Attempting to create a new $TRAVIS_TAG release" 11 | json="{ 12 | \"tag_name\": \"$TRAVIS_TAG\", 13 | \"name\": \"$TRAVIS_TAG\", 14 | \"body\": \"Release of $TRAVIS_TAG: [changelog](https://github.com/resin-io/edge-node-manager/blob/master/CHANGELOG.md)\n$1\" 15 | }" 16 | 17 | resp=$(curl -i --data "$json" --header "Content-Type:application/json" \ 18 | "https://api.github.com/repos/$ACCOUNT/$REPO/releases?access_token=$ACCESS_TOKEN" | \ 19 | head -n 1 | cut -d$' ' -f2) 20 | 21 | if [ $resp = "201" ]; then 22 | echo "Success" 23 | elif [ $resp = "422" ]; then 24 | echo "Release already exists, appending instead" 25 | 26 | release=$(curl https://api.github.com/repos/$ACCOUNT/$REPO/releases/tags/$TRAVIS_TAG) 27 | id=$(echo $release | jq .id) 28 | body=$(echo $release | jq .body) 29 | body="${body%\"}" 30 | body="${body#\"}" 31 | 32 | json="{ 33 | \"body\": \"$body\n$1\" 34 | }" 35 | 36 | resp=$(curl --data "$json" --header "Content-Type:application/json" \ 37 | -X PATCH "https://api.github.com/repos/$ACCOUNT/$REPO/releases/$id?access_token=$ACCESS_TOKEN" | \ 38 | head -n 1 | cut -d$' ' -f2) 39 | 40 | if [ $resp = "200" ]; then 41 | exit 0 42 | else 43 | exit 1 44 | fi 45 | fi 46 | -------------------------------------------------------------------------------- /supervisor/supervisor.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "path" 9 | "strconv" 10 | "time" 11 | 12 | log "github.com/Sirupsen/logrus" 13 | "github.com/cavaliercoder/grab" 14 | "github.com/parnurzeal/gorequest" 15 | "github.com/resin-io/edge-node-manager/config" 16 | ) 17 | 18 | var ( 19 | address string 20 | version string 21 | key string 22 | rawKey string 23 | ) 24 | 25 | func WaitUntilReady() { 26 | log.Info("Waiting until supervisor is ready") 27 | 28 | delay, err := config.GetSupervisorCheckDelay() 29 | if err != nil { 30 | log.WithFields(log.Fields{ 31 | "Error": err, 32 | }).Fatal("Unable to load supervisor check delay") 33 | } 34 | 35 | for { 36 | resp, _, errs := gorequest.New().Timeout(1 * time.Second).Get(address).End() 37 | if errs == nil && resp.StatusCode == 401 { 38 | // The supervisor is up once a 401 status code is returned 39 | log.Info("Supervisor is ready") 40 | return 41 | } 42 | 43 | time.Sleep(delay) 44 | } 45 | } 46 | 47 | func DependentApplicationsList() ([]byte, []error) { 48 | url, err := buildPath(address, []string{version, "dependent-apps"}) 49 | if err != nil { 50 | return nil, []error{err} 51 | } 52 | 53 | req := gorequest.New() 54 | req.Get(url) 55 | req.Query(key) 56 | 57 | log.WithFields(log.Fields{ 58 | "URL": req.Url, 59 | "Method": req.Method, 60 | "Query": req.QueryData, 61 | }).Debug("Requesting dependent applications list") 62 | 63 | resp, body, errs := req.EndBytes() 64 | if errs = handleResp(resp, errs, 200); errs != nil { 65 | return nil, errs 66 | } 67 | 68 | return body, nil 69 | } 70 | 71 | // DependentApplicationUpdate downloads the binary.tar for a specific application and target commit 72 | // Saving it to {ENM_ASSETS_DIRECTORY}/{applicationUUID}/{targetCommit}/binary.tar 73 | func DependentApplicationUpdate(applicationUUID int, targetCommit string) error { 74 | url, err := buildPath(address, []string{version, "dependent-apps", strconv.Itoa(applicationUUID), "assets", targetCommit}) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | req, err := grab.NewRequest(url) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | q := req.HTTPRequest.URL.Query() 85 | q.Set("apikey", rawKey) 86 | req.HTTPRequest.URL.RawQuery = q.Encode() 87 | 88 | filePath := config.GetAssetsDir() 89 | filePath = path.Join(filePath, strconv.Itoa(applicationUUID)) 90 | filePath = path.Join(filePath, targetCommit) 91 | if err = os.MkdirAll(filePath, os.ModePerm); err != nil { 92 | return err 93 | } 94 | filePath = path.Join(filePath, "binary.tar") 95 | req.Filename = filePath 96 | 97 | log.WithFields(log.Fields{ 98 | "URL": req.HTTPRequest.URL, 99 | "Method": req.HTTPRequest.Method, 100 | "Query": req.HTTPRequest.URL.RawQuery, 101 | "Destination": req.Filename, 102 | }).Debug("Requesting dependent application update") 103 | 104 | client := grab.NewClient() 105 | resp, err := client.Do(req) 106 | 107 | if err != nil { 108 | return err 109 | } 110 | 111 | if resp.HTTPResponse.StatusCode != 200 { 112 | return fmt.Errorf("Dependent application update failed") 113 | } 114 | 115 | log.Debug("Dependent application update succeeded") 116 | 117 | return nil 118 | } 119 | 120 | func DependentDeviceLog(UUID, message string) []error { 121 | url, err := buildPath(address, []string{version, "devices", UUID, "logs"}) 122 | if err != nil { 123 | return []error{err} 124 | } 125 | 126 | type dependentDeviceLog struct { 127 | Message string `json:"message"` 128 | } 129 | 130 | content := &dependentDeviceLog{ 131 | Message: message, 132 | } 133 | 134 | bytes, err := json.Marshal(content) 135 | if err != nil { 136 | return []error{err} 137 | } 138 | 139 | req := gorequest.New() 140 | req.Post(url) 141 | req.Set("Content-Type", "application/json") 142 | req.Query(key) 143 | req.Send((string)(bytes)) 144 | 145 | log.WithFields(log.Fields{ 146 | "URL": req.Url, 147 | "Method": req.Method, 148 | "Query": req.QueryData, 149 | "Body": (string)(bytes), 150 | }).Debug("Transmitting dependent device log") 151 | 152 | resp, _, errs := req.End() 153 | return handleResp(resp, errs, 202) 154 | } 155 | 156 | func DependentDeviceInfoUpdateWithOnlineState(UUID, status, commit string, online bool) []error { 157 | url, err := buildPath(address, []string{version, "devices", UUID}) 158 | if err != nil { 159 | return []error{err} 160 | } 161 | 162 | type dependentDeviceInfo struct { 163 | Status string `json:"status"` 164 | Online bool `json:"is_online"` 165 | Commit string `json:"commit,omitempty"` 166 | } 167 | 168 | content := &dependentDeviceInfo{ 169 | Status: status, 170 | Online: online, 171 | Commit: commit, 172 | } 173 | 174 | bytes, err := json.Marshal(content) 175 | if err != nil { 176 | return []error{err} 177 | } 178 | 179 | req := gorequest.New() 180 | req.Put(url) 181 | req.Set("Content-Type", "application/json") 182 | req.Query(key) 183 | req.Send((string)(bytes)) 184 | 185 | log.WithFields(log.Fields{ 186 | "URL": req.Url, 187 | "Method": req.Method, 188 | "Query": req.QueryData, 189 | "Body": (string)(bytes), 190 | }).Debug("Transmitting dependent device info") 191 | 192 | resp, _, errs := req.End() 193 | return handleResp(resp, errs, 200) 194 | } 195 | 196 | func DependentDeviceInfoUpdateWithoutOnlineState(UUID, status, commit string) []error { 197 | url, err := buildPath(address, []string{version, "devices", UUID}) 198 | if err != nil { 199 | return []error{err} 200 | } 201 | 202 | type dependentDeviceInfo struct { 203 | Status string `json:"status"` 204 | Commit string `json:"commit,omitempty"` 205 | } 206 | 207 | content := &dependentDeviceInfo{ 208 | Status: status, 209 | Commit: commit, 210 | } 211 | 212 | bytes, err := json.Marshal(content) 213 | if err != nil { 214 | return []error{err} 215 | } 216 | 217 | req := gorequest.New() 218 | req.Put(url) 219 | req.Set("Content-Type", "application/json") 220 | req.Query(key) 221 | req.Send((string)(bytes)) 222 | 223 | log.WithFields(log.Fields{ 224 | "URL": req.Url, 225 | "Method": req.Method, 226 | "Query": req.QueryData, 227 | "Body": (string)(bytes), 228 | }).Debug("Transmitting dependent device info") 229 | 230 | resp, _, errs := req.End() 231 | return handleResp(resp, errs, 200) 232 | } 233 | 234 | func DependentDeviceInfo(UUID string) ([]byte, []error) { 235 | url, err := buildPath(address, []string{version, "devices", UUID}) 236 | if err != nil { 237 | return nil, []error{err} 238 | } 239 | 240 | req := gorequest.New() 241 | req.Get(url) 242 | req.Query(key) 243 | 244 | log.WithFields(log.Fields{ 245 | "URL": req.Url, 246 | "Method": req.Method, 247 | "Query": req.QueryData, 248 | }).Debug("Requesting dependent device info") 249 | 250 | resp, body, errs := req.EndBytes() 251 | if errs = handleResp(resp, errs, 200); errs != nil { 252 | return nil, errs 253 | } 254 | 255 | return body, nil 256 | } 257 | 258 | func DependentDeviceProvision(applicationUUID int) (resinUUID, name string, errs []error) { 259 | url, err := buildPath(address, []string{version, "devices"}) 260 | if err != nil { 261 | errs = []error{err} 262 | return 263 | } 264 | 265 | type dependentDeviceProvision struct { 266 | ApplicationUUID int `json:"appId"` 267 | } 268 | 269 | content := &dependentDeviceProvision{ 270 | ApplicationUUID: applicationUUID, 271 | } 272 | 273 | bytes, err := json.Marshal(content) 274 | if err != nil { 275 | errs = []error{err} 276 | return 277 | } 278 | 279 | req := gorequest.New() 280 | req.Post(url) 281 | req.Set("Content-Type", "application/json") 282 | req.Query(key) 283 | req.Send((string)(bytes)) 284 | 285 | log.WithFields(log.Fields{ 286 | "URL": req.Url, 287 | "Method": req.Method, 288 | "Query": req.QueryData, 289 | "Body": (string)(bytes), 290 | }).Debug("Requesting dependent device provision") 291 | 292 | resp, body, errs := req.EndBytes() 293 | if errs = handleResp(resp, errs, 201); errs != nil { 294 | return 295 | } 296 | 297 | var buffer map[string]interface{} 298 | if err := json.Unmarshal(body, &buffer); err != nil { 299 | errs = []error{err} 300 | return 301 | } 302 | 303 | resinUUID = buffer["uuid"].(string) 304 | name = buffer["device_name"].(string) 305 | 306 | return 307 | } 308 | 309 | func DependentDevicesList() ([]byte, []error) { 310 | url, err := buildPath(address, []string{version, "devices"}) 311 | if err != nil { 312 | return nil, []error{err} 313 | } 314 | 315 | req := gorequest.New() 316 | req.Get(url) 317 | req.Query(key) 318 | 319 | log.WithFields(log.Fields{ 320 | "URL": req.Url, 321 | "Method": req.Method, 322 | "Query": req.QueryData, 323 | }).Debug("Requesting dependent devices list") 324 | 325 | resp, body, errs := req.EndBytes() 326 | if errs = handleResp(resp, errs, 200); errs != nil { 327 | return nil, errs 328 | } 329 | 330 | return body, nil 331 | } 332 | 333 | func init() { 334 | log.SetLevel(config.GetLogLevel()) 335 | 336 | address = config.GetSuperAddr() 337 | version = config.GetVersion() 338 | rawKey = config.GetSuperAPIKey() 339 | 340 | type apiKey struct { 341 | APIKey string `json:"apikey"` 342 | } 343 | 344 | content := &apiKey{ 345 | APIKey: rawKey, 346 | } 347 | 348 | bytes, err := json.Marshal(content) 349 | if err != nil { 350 | log.WithFields(log.Fields{ 351 | "Key": rawKey, 352 | "Error": err, 353 | }).Fatal("Unable to marshall API key") 354 | } 355 | key = (string)(bytes) 356 | 357 | log.WithFields(log.Fields{ 358 | "Address": address, 359 | "Version": version, 360 | "Key": key, 361 | "Raw key": rawKey, 362 | }).Debug("Initialised outgoing supervisor API") 363 | } 364 | 365 | func buildPath(base string, paths []string) (string, error) { 366 | url, err := url.ParseRequestURI(address) 367 | if err != nil { 368 | return "", err 369 | } 370 | 371 | for _, p := range paths { 372 | url.Path = path.Join(url.Path, p) 373 | } 374 | 375 | return url.String(), nil 376 | } 377 | 378 | func handleResp(resp gorequest.Response, errs []error, statusCode int) []error { 379 | if errs != nil { 380 | return errs 381 | } 382 | 383 | // Allow 404 and 410 here as it means the dep. app or dep. device has just been deleted 384 | if resp.StatusCode != statusCode && resp.StatusCode != 404 && resp.StatusCode != 410 { 385 | return []error{fmt.Errorf("Invalid response received: %s", resp.Status)} 386 | } 387 | 388 | log.WithFields(log.Fields{ 389 | "Response": resp.Status, 390 | }).Debug("Valid response received") 391 | 392 | return nil 393 | } 394 | -------------------------------------------------------------------------------- /versionist.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const execSync = require('child_process').execSync; 4 | 5 | const getAuthor = (commitHash) => { 6 | return execSync(`git show --quiet --format="%an" ${commitHash}`, { 7 | encoding: 'utf8' 8 | }).replace('\n', ''); 9 | }; 10 | 11 | const isIncrementalCommit = (changeType) => { 12 | return Boolean(changeType) && changeType.trim().toLowerCase() !== 'none'; 13 | }; 14 | 15 | module.exports = { 16 | // This setup allows the editing and parsing of footer tags to get version and type information, 17 | // as well as ensuring tags of the type 'v..' are used. 18 | // It increments in a semver compatible fashion and allows the updating of NPM package info. 19 | editChangelog: true, 20 | parseFooterTags: true, 21 | getGitReferenceFromVersion: 'v-prefix', 22 | incrementVersion: 'semver', 23 | updateVersion: (cwd, version, cb) => { cb(); }, 24 | 25 | // Always add the entry to the top of the Changelog, below the header. 26 | addEntryToChangelog: { 27 | preset: 'prepend', 28 | fromLine: 6 29 | }, 30 | 31 | // Only include a commit when there is a footer tag of 'change-type'. 32 | // Ensures commits which do not up versions are not included. 33 | includeCommitWhen: (commit) => { 34 | return isIncrementalCommit(commit.footer['change-type']); 35 | }, 36 | 37 | // Determine the type from 'change-type:' tag. 38 | // Should no explicit change type be made, then no changes are assumed. 39 | getIncrementLevelFromCommit: (commit) => { 40 | if (isIncrementalCommit(commit.footer['change-type'])) { 41 | return commit.footer['change-type'].trim().toLowerCase(); 42 | } 43 | }, 44 | 45 | // If a 'changelog-entry' tag is found, use this as the subject rather than the 46 | // first line of the commit. 47 | transformTemplateData: (data) => { 48 | data.commits.forEach((commit) => { 49 | commit.subject = commit.footer['changelog-entry'] || commit.subject; 50 | commit.author = getAuthor(commit.hash); 51 | }); 52 | 53 | return data; 54 | }, 55 | 56 | template: [ 57 | '## v{{version}} - {{moment date "Y-MM-DD"}}', 58 | '', 59 | '{{#each commits}}', 60 | '{{#if this.author}}', 61 | '* {{capitalize this.subject}} [{{this.author}}]', 62 | '{{else}}', 63 | '* {{capitalize this.subject}}', 64 | '{{/if}}', 65 | '{{/each}}' 66 | ].join('\n') 67 | }; 68 | --------------------------------------------------------------------------------