<repo_name>Bucknalla/edge-node-manager<file_sep>/device/device.go
package device

import (
	"encoding/json"
	"fmt"

	"github.com/resin-io/edge-node-manager/board"
	"github.com/resin-io/edge-node-manager/board/esp8266"
	"github.com/resin-io/edge-node-manager/board/microbit"
	"github.com/resin-io/edge-node-manager/board/nrf51822dk"
	"github.com/resin-io/edge-node-manager/device/hook"
	"github.com/resin-io/edge-node-manager/device/status"
	"github.com/resin-io/edge-node-manager/micro/nrf51822"
	"github.com/resin-io/edge-node-manager/supervisor"
)

type Device struct {
	Board             board.Interface        `json:"-"`
	ApplicationUUID   int                    `storm:"index"`
	BoardType         board.Type             `storm:"index"`
	Name              string                 `storm:"index"`
	LocalUUID         string                 `storm:"index"`
	ResinUUID         string                 `storm:"id,unique,index"`
	Commit            string                 `storm:"index"`
	TargetCommit      string                 `storm:"index"`
	Status            status.Status          `storm:"index"`
	Config            map[string]interface{} `storm:"index"`
	TargetConfig      map[string]interface{} `storm:"index"`
	Environment       map[string]interface{} `storm:"index"`
	TargetEnvironment map[string]interface{} `storm:"index"`
	RestartFlag       bool                   `storm:"index"`
	DeleteFlag        bool                   `storm:"index"`
}

func (d Device) String() string {
	return fmt.Sprintf(
		"Application UUID: %d, "+
			"Board type: %s, "+
			"Name: %s, "+
			"Local UUID: %s, "+
			"Resin UUID: %s, "+
			"Commit: %s, "+
			"Target commit: %s, "+
			"Status: %s, "+
			"Config: %v, "+
			"Target config: %v, "+
			"Environment: %v, "+
			"Target environment: %v, "+
			"Restart: %t, "+
			"Delete: %t",
		d.ApplicationUUID,
		d.BoardType,
		d.Name,
		d.LocalUUID,
		d.ResinUUID,
		d.Commit,
		d.TargetCommit,
		d.Status,
		d.Config,
		d.TargetConfig,
		d.Environment,
		d.TargetEnvironment,
		d.RestartFlag,
		d.DeleteFlag)
}

func New(applicationUUID int, boardType board.Type, name, localUUID, resinUUID string) Device {
	return Device{
		ApplicationUUID: applicationUUID,
		BoardType:       boardType,
		Name:            name,
		LocalUUID:       localUUID,
		ResinUUID:       resinUUID,
		Commit:          "",
		TargetCommit:    "",
		Status:          status.OFFLINE,
	}
}

func (d *Device) PopulateBoard() error {
	log := hook.Create(d.ResinUUID)

	switch d.BoardType {
	case board.MICROBIT:
		d.Board = microbit.Microbit{
			Log: log,
			Micro: nrf51822.Nrf51822{
				Log:                 log,
				LocalUUID:           d.LocalUUID,
				Firmware:            nrf51822.FIRMWARE{},
				NotificationChannel: make(chan []byte),
			},
		}
	case board.NRF51822DK:
		d.Board = nrf51822dk.Nrf51822dk{
			Log: log,
			Micro: nrf51822.Nrf51822{
				Log:                 log,
				LocalUUID:           d.LocalUUID,
				Firmware:            nrf51822.FIRMWARE{},
				NotificationChannel: make(chan []byte),
			},
		}
	case board.ESP8266:
		d.Board = esp8266.Esp8266{
			Log:       log,
			LocalUUID: d.LocalUUID,
		}
	default:
		return fmt.Errorf("Unsupported board type")
	}

	return nil
}

// Sync device with resin to ensure we have the latest values for:
// - Device name
// - Device target config
// - Device target environment
func (d *Device) Sync() []error {
	bytes, errs := supervisor.DependentDeviceInfo(d.ResinUUID)
	if errs != nil {
		return errs
	}

	var temp Device
	if err := json.Unmarshal(bytes, &temp); err != nil {
		// Ignore the error here as it means the device we are trying
		// to sync has been deleted
		return nil
	}

	d.Name = temp.Name
	d.TargetConfig = temp.TargetConfig
	d.TargetEnvironment = temp.TargetEnvironment

	return nil
}
<file_sep>/radio/bluetooth/bluetooth.go
package bluetooth

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/pkg/errors"
	"golang.org/x/net/context"

	log "github.com/Sirupsen/logrus"
	"github.com/currantlabs/ble"
	"github.com/currantlabs/ble/linux"
	"github.com/currantlabs/ble/linux/hci"
	"github.com/currantlabs/ble/linux/hci/cmd"
	"github.com/resin-io/edge-node-manager/config"
)

var (
	initialised  bool
	doneChannel  chan struct{}
	name         *ble.Characteristic
	shortTimeout time.Duration
	longTimeout  time.Duration
)

func Initialise() error {
	if !initialised && os.Getenv("RESIN_DEVICE_TYPE") == "raspberrypi3" {
		log.Info("Initialising bluetooth")

		for i := 1; i <= 3; i++ {
			if err := exec.Command("bash", "-c", "/usr/bin/hciattach /dev/ttyAMA0 bcm43xx 921600 noflow -").Run(); err == nil {
				if err := exec.Command("bash", "-c", "hciconfig hci0 up").Run(); err != nil {
					return err
				}

				log.Info("Initialised bluetooth")

				initialised = true
				break
			}
		}

		// Small sleep to give the bluetooth interface time to settle
		time.Sleep(shortTimeout)
	}

	device, err := linux.NewDevice()
	if err != nil {
		return err
	}

	if err := updateLinuxParam(device); err != nil {
		return err
	}

	ble.SetDefaultDevice(device)

	return nil
}

func Cleanup() error {
	return ble.Stop()
}

func Connect(id string) (ble.Client, error) {
	client, err := ble.Dial(ble.WithSigHandler(context.WithTimeout(context.Background(), longTimeout)), hci.RandomAddress{ble.NewAddr(id)})
	if err != nil {
		return nil, err
	}

	if _, err := client.ExchangeMTU(ble.MaxMTU); err != nil {
		return nil, err
	}

	doneChannel = make(chan struct{})
	go func() {
		<-client.Disconnected()
		close(doneChannel)
	}()

	return client, nil
}

func Disconnect(client ble.Client) error {
	if err := client.ClearSubscriptions(); err != nil {
		return err
	}

	if err := client.CancelConnection(); err != nil {
		return err
	}
	<-doneChannel

	return nil
}

func WriteCharacteristic(client ble.Client, characteristic *ble.Characteristic, value []byte, noRsp bool) error {
	err := make(chan error)
	go func() {
		err <- client.WriteCharacteristic(characteristic, value, noRsp)
	}()

	select {
	case done := <-err:
		return done
	case <-time.After(shortTimeout):
		return fmt.Errorf("Write characteristic timed out")
	}
}

func ReadCharacteristic(client ble.Client, characteristic *ble.Characteristic) ([]byte, error) {
	type Result struct {
		Val []byte
		Err error
	}

	result := make(chan Result)
	go func() {
		result <- func() Result {
			val, err := client.ReadCharacteristic(characteristic)
			return Result{val, err}
		}()
	}()

	select {
	case done := <-result:
		return done.Val, done.Err
	case <-time.After(shortTimeout):
		return nil, fmt.Errorf("Read characteristic timed out")
	}
}

func WriteDescriptor(client ble.Client, descriptor *ble.Descriptor, value []byte) error {
	err := make(chan error)
	go func() {
		err <- client.WriteDescriptor(descriptor, value)
	}()

	select {
	case done := <-err:
		return done
	case <-time.After(shortTimeout):
		return fmt.Errorf("Write descriptor timed out")
	}
}

func Scan(id string) (map[string]struct{}, error) {
	devices := make(map[string]struct{})
	advChannel := make(chan ble.Advertisement)
	ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), longTimeout))

	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case adv := <-advChannel:
				if strings.EqualFold(adv.LocalName(), id) {
					var s struct{}
					devices[adv.Address().String()] = s
				}
			}
		}
	}()

	err := ble.Scan(ctx, false, func(adv ble.Advertisement) { advChannel <- adv }, nil)
	if errors.Cause(err) != context.DeadlineExceeded && errors.Cause(err) != context.Canceled {
		return devices, err
	}

	return devices, nil
}

func Online(id string) (bool, error) {
	online := false
	advChannel := make(chan ble.Advertisement)
	ctx, cancel := context.WithCancel(context.Background())
	ctx = ble.WithSigHandler(context.WithTimeout(ctx, longTimeout))

	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case adv := <-advChannel:
				if strings.EqualFold(adv.Address().String(), id) {
					online = true
					cancel()
				}
			}
		}
	}()

	err := ble.Scan(ctx, false, func(adv ble.Advertisement) { advChannel <- adv }, nil)
	if errors.Cause(err) != context.DeadlineExceeded && errors.Cause(err) != context.Canceled {
		return online, err
	}

	return online, nil
}

func GetName(id string) (string, error) {
	client, err := Connect(id)
	if err != nil {
		return "", err
	}

	resp, err := ReadCharacteristic(client, name)
	if err != nil {
		return "", err
	}

	if err := Disconnect(client); err != nil {
		return "", err
	}

	return string(resp), nil
}

func GetCharacteristic(uuid string, property ble.Property, handle, vhandle uint16) (*ble.Characteristic, error) {
	parsedUUID, err := ble.Parse(uuid)
	if err != nil {
		return nil, err
	}

	characteristic := ble.NewCharacteristic(parsedUUID)
	characteristic.Property = property
	characteristic.Handle = handle
	characteristic.ValueHandle = vhandle

	return characteristic, nil
}

func GetDescriptor(uuid string, handle uint16) (*ble.Descriptor, error) {
	parsedUUID, err := ble.Parse(uuid)
	if err != nil {
		return nil, err
	}

	descriptor := ble.NewDescriptor(parsedUUID)
	descriptor.Handle = handle

	return descriptor, nil
}

func init() {
	log.SetLevel(config.GetLogLevel())

	var err error
	if shortTimeout, err = config.GetShortBluetoothTimeout(); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to load bluetooth timeout")
	}

	if longTimeout, err = config.GetLongBluetoothTimeout(); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to load bluetooth timeout")
	}

	name, err = GetCharacteristic("2a00", ble.CharRead+ble.CharWrite, 0x02, 0x03)
	if err != nil {
		log.Fatal(err)
	}

	log.Debug("Initialised bluetooth radio")
}

func updateLinuxParam(device *linux.Device) error {
	if err := device.HCI.Send(&cmd.LESetScanParameters{
		LEScanType:           0x00,   // 0x00: passive, 0x01: active
		LEScanInterval:       0x0060, // 0x0004 - 0x4000; N * 0.625msec
		LEScanWindow:         0x0060, // 0x0004 - 0x4000; N * 0.625msec
		OwnAddressType:       0x01,   // 0x00: public, 0x01: random
		ScanningFilterPolicy: 0x00,   // 0x00: accept all, 0x01: ignore non-white-listed.
	}, nil); err != nil {
		return errors.Wrap(err, "can't set scan param")
	}

	if err := device.HCI.Option(hci.OptConnParams(
		cmd.LECreateConnection{
			LEScanInterval:        0x0060,    // 0x0004 - 0x4000; N * 0.625 msec
			LEScanWindow:          0x0060,    // 0x0004 - 0x4000; N * 0.625 msec
			InitiatorFilterPolicy: 0x00,      // White list is not used
			PeerAddressType:       0x00,      // Public Device Address
			PeerAddress:           [6]byte{}, //
			OwnAddressType:        0x00,      // Public Device Address
			ConnIntervalMin:       0x0028,    // 0x0006 - 0x0C80; N * 1.25 msec
			ConnIntervalMax:       0x0038,    // 0x0006 - 0x0C80; N * 1.25 msec
			ConnLatency:           0x0000,    // 0x0000 - 0x01F3; N * 1.25 msec
			SupervisionTimeout:    0x002A,    // 0x000A - 0x0C80; N * 10 msec
			MinimumCELength:       0x0000,    // 0x0000 - 0xFFFF; N * 0.625 msec
			MaximumCELength:       0x0000,    // 0x0000 - 0xFFFF; N * 0.625 msec
		})); err != nil {
		return errors.Wrap(err, "can't set connection param")
	}
	return nil
}
<file_sep>/supervisor/supervisor.go
package supervisor

import (
	"encoding/json"
	"fmt"
	"net/url"
	"os"
	"path"
	"strconv"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/cavaliercoder/grab"
	"github.com/parnurzeal/gorequest"
	"github.com/resin-io/edge-node-manager/config"
)

var (
	address string
	version string
	key     string
	rawKey  string
)

func WaitUntilReady() {
	log.Info("Waiting until supervisor is ready")

	delay, err := config.GetSupervisorCheckDelay()
	if err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to load supervisor check delay")
	}

	for {
		resp, _, errs := gorequest.New().Timeout(1 * time.Second).Get(address).End()
		if errs == nil && resp.StatusCode == 401 {
			// The supervisor is up once a 401 status code is returned
			log.Info("Supervisor is ready")
			return
		}

		time.Sleep(delay)
	}
}

func DependentApplicationsList() ([]byte, []error) {
	url, err := buildPath(address, []string{version, "dependent-apps"})
	if err != nil {
		return nil, []error{err}
	}

	req := gorequest.New()
	req.Get(url)
	req.Query(key)

	log.WithFields(log.Fields{
		"URL":    req.Url,
		"Method": req.Method,
		"Query":  req.QueryData,
	}).Debug("Requesting dependent applications list")

	resp, body, errs := req.EndBytes()
	if errs = handleResp(resp, errs, 200); errs != nil {
		return nil, errs
	}

	return body, nil
}

// DependentApplicationUpdate downloads the binary.tar for a specific application and target commit
// Saving it to {ENM_ASSETS_DIRECTORY}/{applicationUUID}/{targetCommit}/binary.tar
func DependentApplicationUpdate(applicationUUID int, targetCommit string) error {
	url, err := buildPath(address, []string{version, "dependent-apps", strconv.Itoa(applicationUUID), "assets", targetCommit})
	if err != nil {
		return err
	}

	req, err := grab.NewRequest(url)
	if err != nil {
		return err
	}

	q := req.HTTPRequest.URL.Query()
	q.Set("apikey", rawKey)
	req.HTTPRequest.URL.RawQuery = q.Encode()

	filePath := config.GetAssetsDir()
	filePath = path.Join(filePath, strconv.Itoa(applicationUUID))
	filePath = path.Join(filePath, targetCommit)
	if err = os.MkdirAll(filePath, os.ModePerm); err != nil {
		return err
	}
	filePath = path.Join(filePath, "binary.tar")
	req.Filename = filePath

	log.WithFields(log.Fields{
		"URL":         req.HTTPRequest.URL,
		"Method":      req.HTTPRequest.Method,
		"Query":       req.HTTPRequest.URL.RawQuery,
		"Destination": req.Filename,
	}).Debug("Requesting dependent application update")

	client := grab.NewClient()
	resp, err := client.Do(req)

	if err != nil {
		return err
	}

	if resp.HTTPResponse.StatusCode != 200 {
		return fmt.Errorf("Dependent application update failed")
	}

	log.Debug("Dependent application update succeeded")

	return nil
}

func DependentDeviceLog(UUID, message string) []error {
	url, err := buildPath(address, []string{version, "devices", UUID, "logs"})
	if err != nil {
		return []error{err}
	}

	type dependentDeviceLog struct {
		Message string `json:"message"`
	}

	content := &dependentDeviceLog{
		Message: message,
	}

	bytes, err := json.Marshal(content)
	if err != nil {
		return []error{err}
	}

	req := gorequest.New()
	req.Post(url)
	req.Set("Content-Type", "application/json")
	req.Query(key)
	req.Send((string)(bytes))

	log.WithFields(log.Fields{
		"URL":    req.Url,
		"Method": req.Method,
		"Query":  req.QueryData,
		"Body":   (string)(bytes),
	}).Debug("Transmitting dependent device log")

	resp, _, errs := req.End()
	return handleResp(resp, errs, 202)
}

func DependentDeviceInfoUpdateWithOnlineState(UUID, status, commit string, online bool) []error {
	url, err := buildPath(address, []string{version, "devices", UUID})
	if err != nil {
		return []error{err}
	}

	type dependentDeviceInfo struct {
		Status string `json:"status"`
		Online bool   `json:"is_online"`
		Commit string `json:"commit,omitempty"`
	}

	content := &dependentDeviceInfo{
		Status: status,
		Online: online,
		Commit: commit,
	}

	bytes, err := json.Marshal(content)
	if err != nil {
		return []error{err}
	}

	req := gorequest.New()
	req.Put(url)
	req.Set("Content-Type", "application/json")
	req.Query(key)
	req.Send((string)(bytes))

	log.WithFields(log.Fields{
		"URL":    req.Url,
		"Method": req.Method,
		"Query":  req.QueryData,
		"Body":   (string)(bytes),
	}).Debug("Transmitting dependent device info")

	resp, _, errs := req.End()
	return handleResp(resp, errs, 200)
}

func DependentDeviceInfoUpdateWithoutOnlineState(UUID, status, commit string) []error {
	url, err := buildPath(address, []string{version, "devices", UUID})
	if err != nil {
		return []error{err}
	}

	type dependentDeviceInfo struct {
		Status string `json:"status"`
		Commit string `json:"commit,omitempty"`
	}

	content := &dependentDeviceInfo{
		Status: status,
		Commit: commit,
	}

	bytes, err := json.Marshal(content)
	if err != nil {
		return []error{err}
	}

	req := gorequest.New()
	req.Put(url)
	req.Set("Content-Type", "application/json")
	req.Query(key)
	req.Send((string)(bytes))

	log.WithFields(log.Fields{
		"URL":    req.Url,
		"Method": req.Method,
		"Query":  req.QueryData,
		"Body":   (string)(bytes),
	}).Debug("Transmitting dependent device info")

	resp, _, errs := req.End()
	return handleResp(resp, errs, 200)
}

func DependentDeviceInfo(UUID string) ([]byte, []error) {
	url, err := buildPath(address, []string{version, "devices", UUID})
	if err != nil {
		return nil, []error{err}
	}

	req := gorequest.New()
	req.Get(url)
	req.Query(key)

	log.WithFields(log.Fields{
		"URL":    req.Url,
		"Method": req.Method,
		"Query":  req.QueryData,
	}).Debug("Requesting dependent device info")

	resp, body, errs := req.EndBytes()
	if errs = handleResp(resp, errs, 200); errs != nil {
		return nil, errs
	}

	return body, nil
}

func DependentDeviceProvision(applicationUUID int) (resinUUID, name string, errs []error) {
	url, err := buildPath(address, []string{version, "devices"})
	if err != nil {
		errs = []error{err}
		return
	}

	type dependentDeviceProvision struct {
		ApplicationUUID int `json:"appId"`
	}

	content := &dependentDeviceProvision{
		ApplicationUUID: applicationUUID,
	}

	bytes, err := json.Marshal(content)
	if err != nil {
		errs = []error{err}
		return
	}

	req := gorequest.New()
	req.Post(url)
	req.Set("Content-Type", "application/json")
	req.Query(key)
	req.Send((string)(bytes))

	log.WithFields(log.Fields{
		"URL":    req.Url,
		"Method": req.Method,
		"Query":  req.QueryData,
		"Body":   (string)(bytes),
	}).Debug("Requesting dependent device provision")

	resp, body, errs := req.EndBytes()
	if errs = handleResp(resp, errs, 201); errs != nil {
		return
	}

	var buffer map[string]interface{}
	if err := json.Unmarshal(body, &buffer); err != nil {
		errs = []error{err}
		return
	}

	resinUUID = buffer["uuid"].(string)
	name = buffer["device_name"].(string)

	return
}

func DependentDevicesList() ([]byte, []error) {
	url, err := buildPath(address, []string{version, "devices"})
	if err != nil {
		return nil, []error{err}
	}

	req := gorequest.New()
	req.Get(url)
	req.Query(key)

	log.WithFields(log.Fields{
		"URL":    req.Url,
		"Method": req.Method,
		"Query":  req.QueryData,
	}).Debug("Requesting dependent devices list")

	resp, body, errs := req.EndBytes()
	if errs = handleResp(resp, errs, 200); errs != nil {
		return nil, errs
	}

	return body, nil
}

func init() {
	log.SetLevel(config.GetLogLevel())

	address = config.GetSuperAddr()
	version = config.GetVersion()
	rawKey = config.GetSuperAPIKey()

	type apiKey struct {
		APIKey string `json:"apikey"`
	}

	content := &apiKey{
		APIKey: rawKey,
	}

	bytes, err := json.Marshal(content)
	if err != nil {
		log.WithFields(log.Fields{
			"Key":   rawKey,
			"Error": err,
		}).Fatal("Unable to marshall API key")
	}
	key = (string)(bytes)

	log.WithFields(log.Fields{
		"Address": address,
		"Version": version,
		"Key":     key,
		"Raw key": rawKey,
	}).Debug("Initialised outgoing supervisor API")
}

func buildPath(base string, paths []string) (string, error) {
	url, err := url.ParseRequestURI(address)
	if err != nil {
		return "", err
	}

	for _, p := range paths {
		url.Path = path.Join(url.Path, p)
	}

	return url.String(), nil
}

func handleResp(resp gorequest.Response, errs []error, statusCode int) []error {
	if errs != nil {
		return errs
	}

	// Allow 404 and 410 here as it means the dep. app or dep. device has just been deleted
	if resp.StatusCode != statusCode && resp.StatusCode != 404 && resp.StatusCode != 410 {
		return []error{fmt.Errorf("Invalid response received: %s", resp.Status)}
	}

	log.WithFields(log.Fields{
		"Response": resp.Status,
	}).Debug("Valid response received")

	return nil
}
<file_sep>/micro/nrf51822/nrf51822.go
package nrf51822

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io/ioutil"
	"path"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/currantlabs/ble"
	"github.com/mholt/archiver"
	"github.com/resin-io/edge-node-manager/config"
	"github.com/resin-io/edge-node-manager/radio/bluetooth"
)

// Firmware-over-the-air update info
// https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v11.0.0%2Fbledfu_transport_bleprofile.html
// https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v11.0.0%2Fbledfu_transport_bleservice.html&anchor=ota_spec_control_state

const (
	Success            byte = 0x01
	Start                   = 0x01
	Initialise              = 0x02
	Receive                 = 0x03
	Validate                = 0x04
	Activate                = 0x05
	Restart                 = 0x06
	ReceivedSize            = 0x07
	RequestBlockRecipt      = 0x08
	Response                = 0x10
	BlockRecipt             = 0x11
)

// Nrf51822 is a BLE SoC from Nordic
// https://www.nordicsemi.com/eng/Products/Bluetooth-low-energy/nRF51822
type Nrf51822 struct {
	Log                 *log.Logger
	LocalUUID           string
	Firmware            FIRMWARE
	NotificationChannel chan []byte
}

type FIRMWARE struct {
	currentBlock int
	size         int
	binary       []byte
	data         []byte
}

var (
	dfuPkt       *ble.Characteristic
	dfuCtrl      *ble.Characteristic
	shortTimeout time.Duration
	longTimeout  time.Duration
)

func (m *Nrf51822) InitialiseRadio() error {
	return bluetooth.Initialise()
}

func (m *Nrf51822) CleanupRadio() error {
	return bluetooth.Cleanup()
}

func (m *Nrf51822) ExtractFirmware(filePath, bin, data string) error {
	m.Log.WithFields(log.Fields{
		"Firmware path": filePath,
		"Bin":           bin,
		"Data":          data,
	}).Debug("Extracting firmware")

	var err error

	if err = archiver.Zip.Open(path.Join(filePath, "application.zip"), filePath); err != nil {
		return err
	}

	m.Firmware.binary, err = ioutil.ReadFile(path.Join(filePath, bin))
	if err != nil {
		return err
	}

	m.Firmware.data, err = ioutil.ReadFile(path.Join(filePath, data))
	if err != nil {
		return err
	}

	m.Firmware.size = len(m.Firmware.binary)

	m.Log.WithFields(log.Fields{
		"Size": m.Firmware.size,
	}).Debug("Extracted firmware")

	return nil
}

func (m *Nrf51822) Update(client ble.Client) error {
	if err := m.subscribe(client); err != nil {
		return err
	}
	defer client.ClearSubscriptions()

	if err := m.checkFOTA(client); err != nil {
		return err
	}

	if m.Firmware.currentBlock == 0 {
		if err := m.initFOTA(client); err != nil {
			return err
		}
	}

	if err := m.transferFOTA(client); err != nil {
		return err
	}

	if err := m.validateFOTA(client); err != nil {
		return err
	}

	return m.finaliseFOTA(client)
}

func init() {
	log.SetLevel(config.GetLogLevel())

	var err error
	if shortTimeout, err = config.GetShortBluetoothTimeout(); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to load bluetooth timeout")
	}

	if longTimeout, err = config.GetLongBluetoothTimeout(); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to load bluetooth timeout")
	}

	dfuCtrl, err = bluetooth.GetCharacteristic("000015311212efde1523785feabcd123", ble.CharWrite+ble.CharNotify, 0x0F, 0x10)
	if err != nil {
		log.Fatal(err)
	}

	descriptor, err := bluetooth.GetDescriptor("2902", 0x11)
	if err != nil {
		log.Fatal(err)
	}
	dfuCtrl.CCCD = descriptor

	dfuPkt, err = bluetooth.GetCharacteristic("000015321212efde1523785feabcd123", ble.CharWriteNR, 0x0D, 0x0E)
	if err != nil {
		log.Fatal(err)
	}

	log.Debug("Initialised nRF51822 characteristics")
}

func (m *Nrf51822) subscribe(client ble.Client) error {
	if err := bluetooth.WriteDescriptor(client, dfuCtrl.CCCD, []byte{0x0001}); err != nil {
		return err
	}

	return client.Subscribe(dfuCtrl, false, func(b []byte) {
		m.NotificationChannel <- b
	})
}

func (m *Nrf51822) checkFOTA(client ble.Client) error {
	m.Log.Debug("Checking FOTA")

	if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{ReceivedSize}, false); err != nil {
		return err
	}

	resp, err := m.getNotification([]byte{Response, ReceivedSize, Success}, true)
	if err != nil {
		return err
	}

	m.Firmware.currentBlock, err = unpack(resp[3:])
	if err != nil {
		return err
	}

	m.Log.WithFields(log.Fields{
		"Start block": m.Firmware.currentBlock,
	}).Debug("Checked FOTA")

	return nil
}

func (m *Nrf51822) initFOTA(client ble.Client) error {
	m.Log.Debug("Initialising FOTA")

	if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Start, 0x04}, false); err != nil {
		return err
	}

	buf := new(bytes.Buffer)
	if _, err := buf.Write(make([]byte, 8)); err != nil {
		return err
	}

	if err := binary.Write(buf, binary.LittleEndian, (int32)(m.Firmware.size)); err != nil {
		return err
	}

	if err := bluetooth.WriteCharacteristic(client, dfuPkt, buf.Bytes(), false); err != nil {
		return err
	}

	if _, err := m.getNotification([]byte{Response, Start, Success}, true); err != nil {
		return err
	}

	if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Initialise, 0x00}, false); err != nil {
		return err
	}

	if err := bluetooth.WriteCharacteristic(client, dfuPkt, m.Firmware.data, false); err != nil {
		return err
	}

	if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Initialise, 0x01}, false); err != nil {
		return err
	}

	if _, err := m.getNotification([]byte{Response, Initialise, Success}, true); err != nil {
		return err
	}

	if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{RequestBlockRecipt, 0x64, 0x00}, false); err != nil {
		return err
	}

	if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Receive}, false); err != nil {
		return err
	}

	m.Log.Debug("Initialised FOTA")

	return nil
}

func (m *Nrf51822) transferFOTA(client ble.Client) error {
	blockCounter := 1
	blockSize := 20

	if m.Firmware.currentBlock != 0 {
		blockCounter += (m.Firmware.currentBlock / blockSize)
	}

	m.Log.WithFields(log.Fields{
		"Progress %": m.getProgress(),
	}).Info("Transferring FOTA")

	for i := m.Firmware.currentBlock; i < m.Firmware.size; i += blockSize {
		sliceIndex := i + blockSize
		if sliceIndex > m.Firmware.size {
			sliceIndex = m.Firmware.size
		}
		block := m.Firmware.binary[i:sliceIndex]

		if err := bluetooth.WriteCharacteristic(client, dfuPkt, block, true); err != nil {
			return err
		}

		if (blockCounter % 100) == 0 {
			resp, err := m.getNotification(nil, false)
			if err != nil {
				return err
			}

			if resp[0] != BlockRecipt {
				return fmt.Errorf("Incorrect notification received")
			}

			if m.Firmware.currentBlock, err = unpack(resp[1:]); err != nil {
				return err
			}

			if (i + blockSize) != m.Firmware.currentBlock {
				return fmt.Errorf("FOTA transer out of sync")
			}

			m.Log.WithFields(log.Fields{
				"Progress %": m.getProgress(),
			}).Info("Transferring FOTA")
		}

		blockCounter++
	}

	if _, err := m.getNotification([]byte{Response, Receive, Success}, true); err != nil {
		return err
	}

	m.Log.WithFields(log.Fields{
		"Progress %": 100,
	}).Info("Transferring FOTA")

	return nil
}

func (m *Nrf51822) validateFOTA(client ble.Client) error {
	m.Log.Debug("Validating FOTA")

	if err := m.checkFOTA(client); err != nil {
		return err
	}

	if m.Firmware.currentBlock != m.Firmware.size {
		return fmt.Errorf("Bytes received does not match binary size")
	}

	if err := bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Validate}, false); err != nil {
		return err
	}

	if _, err := m.getNotification([]byte{Response, Validate, Success}, true); err != nil {
		return err
	}

	m.Log.Debug("Validated FOTA")

	return nil
}

func (m Nrf51822) finaliseFOTA(client ble.Client) error {
	m.Log.Debug("Finalising FOTA")

	// Ignore the error because this command causes the device to disconnect
	bluetooth.WriteCharacteristic(client, dfuCtrl, []byte{Activate}, false)

	// Give the device time to disconnect
	time.Sleep(shortTimeout)

	m.Log.Debug("Finalised FOTA")

	return nil
}

func (m *Nrf51822) getNotification(exp []byte, compare bool) ([]byte, error) {
	select {
	case <-time.After(longTimeout):
		return nil, fmt.Errorf("Timed out waiting for notification")
	case resp := <-m.NotificationChannel:
		if !compare || bytes.Equal(resp[:3], exp) {
			return resp, nil
		}

		m.Log.WithFields(log.Fields{
			"[0]": fmt.Sprintf("0x%X", resp[0]),
			"[1]": fmt.Sprintf("0x%X", resp[1]),
			"[2]": fmt.Sprintf("0x%X", resp[2]),
		}).Debug("Received")

		m.Log.WithFields(log.Fields{
			"[0]": fmt.Sprintf("0x%X", exp[0]),
			"[1]": fmt.Sprintf("0x%X", exp[1]),
			"[2]": fmt.Sprintf("0x%X", exp[2]),
		}).Debug("Expected")

		return nil, fmt.Errorf("Incorrect notification received")
	}
}

func (m *Nrf51822) getProgress() float32 {
	return ((float32)(m.Firmware.currentBlock) / (float32)(m.Firmware.size)) * 100.0
}

func unpack(resp []byte) (int, error) {
	var result int32
	buf := bytes.NewReader(resp)
	if err := binary.Read(buf, binary.LittleEndian, &result); err != nil {
		return 0, err
	}
	return (int)(result), nil
}
<file_sep>/README.md
# edge-node-manager
[![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)
[![Build Status](https://travis-ci.com/resin-io/edge-node-manager.svg?token=<KEY>&branch=master)](https://travis-ci.com/resin-io/edge-node-manager)

resin.io dependent device edge-node-manager written in Go.

## Getting started
 - Sign up on [resin.io](https://dashboard.resin.io/signup)
 - Work through the [getting started
   guide](https://docs.resin.io/raspberrypi3/nodejs/getting-started/)
 - Create a new application
 - Set these variables in the `Fleet Configuration` application side tab
    - `RESIN_SUPERVISOR_DELTA=1`
    - `RESIN_UI_ENABLE_DEPENDENT_APPLICATIONS=1`
 - Clone this repository to your local workspace
 - Add the dependent application `resin remote` to your local workspace
 - Provision a gateway device
 - Push code to resin as normal :)
 - Follow the readme of the [supported dependent
   device](#supported-dependent-devices) you would like to use

## Configuration variables
More info about environment variables can be found in
the [documentation](https://docs.resin.io/management/env-vars/). If you
don't set environment variables the default will be used.

Environment Variable | Default | Description
------------ | ------------- | -------------
ENM_LOG_LEVEL | `info` | the edge-node-manager log level
DEPENDENT_LOG_LEVEL | `info` | the dependent device log level
ENM_SUPERVISOR_CHECK_DELAY | `1` | the time delay in seconds between each supervisor check at startup
ENM_HOTSPOT_DELETE_DELAY | `10` | the time delay in seconds between hotspot deletion and creation
ENM_CONFIG_LOOP_DELAY | `10` | the time delay in seconds between each application process loop
ENM_CONFIG_PAUSE_DELAY | `10` | the time delay in seconds between each pause check
ENM_HOTSPOT_SSID | `resin-hotspot` | the SSID used for the hotspot
ENM_HOTSPOT_PASSWORD | `<PASSWORD>` | the password used for the hotspot
ENM_BLUETOOTH_SHORT_TIMEOUT | `1` | the timeout in seconds for instantaneous bluetooth operations
ENM_BLUETOOTH_LONG_TIMEOUT | `10` | the timeout in seconds for long running bluetooth operations
ENM_AVAHI_TIMEOUT | `10` | the timeout in seconds for Avahi scan operations
ENM_UPDATE_RETRIES | `1` | the number of times the firmware update process should be retried
ENM_ASSETS_DIRECTORY | `/data/assets` | the root directory used to store the dependent device firmware
ENM_DB_DIRECTORY | `/data/database` | the root directory used to store the database
ENM_DB_FILE | `enm.db` | the database file name
ENM_API_VERSION | `v1` | the proxyvisor API version
RESIN_SUPERVISOR_ADDRESS | `http://127.0.0.1:4000` | the address used to communicate with the proxyvisor
RESIN_SUPERVISOR_API_KEY | `na` | the api key used to communicate with the proxyvisor
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

## API
The edge-node-manager provides an API that allows the user to set the
target status of the main process. This is useful to free up the on-board radios
allowing user code to interact directly with the dependent devices e.g. to
collect sensor data.

**Warning** - Do not try and interact with the on-board radios whilst the
edge-node-manager is running (this leads to inconsistent, unexpected behaviour).

### SET /v1/enm/status
Set the edge-node-manager process status.

#### Example
```
curl -i -H "Content-Type: application/json" -X PUT --data \
'{"targetStatus":"Paused"}' localhost:1337/v1/enm/status

curl -i -H "Content-Type: application/json" -X PUT --data \
'{"targetStatus":"Running"}' localhost:1337/v1/enm/status
```

#### Response
```
HTTP/1.1 200 OK
```

### GET /v1/enm/status
Get the edge-node-manager process status.

#### Example
```
curl -i -X GET localhost:1337/v1/enm/status
```

#### Response
```
HTTP/1.1 200 OK
{
    "currentStatus":"Running",
    "targetStatus":"Paused",
}
```

### GET /v1/devices
Get all dependent devices.

#### Example
```
curl -i -X GET localhost:1337/v1/devices
```

#### Response
```
HTTP/1.1 200 OK
[{
	"ApplicationUUID": 511898,
	"BoardType": "esp8266",
	"Name": "holy-sunset",
	"LocalUUID": "1265892",
	"ResinUUID": "64a1ae375b213d7e5af8409da3ad63108df4c8462089a05aa9af358c3f0df1",
	"Commit": "16b5cd4df8085d2872a6f6fc0c378629a185d78b",
	"TargetCommit": "16b5cd4df8085d2872a6f6fc0c378629a185d78b",
	"Status": "Idle",
	"Config": null,
	"TargetConfig": {
		"RESIN_HOST_TYPE": "esp8266",
		"RESIN_SUPERVISOR_DELTA": "1"
	},
	"Environment": null,
	"TargetEnvironment": {},
	"RestartFlag": false,
	"DeleteFlag": false
}]
```

### GET /v1/devices/{uuid}
Get a dependent device.

#### Example
```
curl -i -X GET localhost:1337/v1/devices/1265892
```

#### Response
```
HTTP/1.1 200 OK
{
	"ApplicationUUID": 511898,
	"BoardType": "esp8266",
	"Name": "holy-sunset",
	"LocalUUID": "1265892",
	"ResinUUID": "64a1ae375b213d7e5af8409da3ad63108df4c8462089a05aa9af358c3f0df1",
	"Commit": "<PASSWORD>",
	"TargetCommit": "<PASSWORD>",
	"Status": "Idle",
	"Config": null,
	"TargetConfig": {
		"RESIN_HOST_TYPE": "esp8266",
		"RESIN_SUPERVISOR_DELTA": "1"
	},
	"Environment": null,
	"TargetEnvironment": {},
	"RestartFlag": false,
	"DeleteFlag": false
}
```

## Supported dependent devices
- [micro:bit](https://github.com/resin-io-projects/micro-bit)
- [nRF51822-DK](https://github.com/resin-io-projects/nRF51822-DK)
- [ESP8266](https://github.com/resin-io-projects/esp8266)

## Further reading
### About
The edge-node-manager is an example of a gateway
application designed to bridge the gap between Resin OS capable single board
computers (e.g. the Raspberry Pi) and non Resin OS capable devices (e.g.
micro-controllers). It has been designed to make it as easy as possible to add
new supported dependent device types and to run alongside your user application.

The following functionality is implemented:
 - Dependent device detection
 - Dependent device provisioning
 - Dependent device restart
 - Dependent device over-the-air (OTA) updating
 - Dependent device logging and information updating
 - API

### Definitions
#### Dependent application
A dependent application is a Resin application that targets devices not capable
of interacting directly with the Resin API.

The dependent application is scoped under a Resin application, which gets the
definition of gateway application.

A dependent application follows the same development cycle as a conventional
Resin application:
 - It binds to your git workspace via the `resin remote`
 - It consists of a Docker application
 - It offers the same environment and configuration variables management

There are some key differences:
 - It does not support Dockerfile templating
 - The Dockerfile must target an x86 base image
 - The actual firmware must be stored in the `/assets` folder within the built
   docker image

#### Dependent device
A dependent device is a device not capable of interacting directly with the
Resin API - the reasons can be several, the most common are:
 - No direct Internet capabilities
 - Not able to run the Resin OS (being a microcontroller, for example)

#### Gateway application
The gateway application is responsible for detecting, provisioning and managing
dependent devices belonging to one of its dependent applications. This is
possible leveraging a new set of endpoints exposed by the [Resin
Supervisor](https://github.com/resin-io/resin-supervisor).

The edge-node-manager (this repository) is an example of a gateway application.

#### Gateway device
The gateway device runs the gateway application and has the needed on-board
radios to communicate with the managed dependent devices, for example:
 - Bluetooth
 - WiFi
 - LoRa
 - ZigBee

Throughout development a Raspberry Pi 3 has been used as the gateway device.
<file_sep>/board/esp8266/esp8266.go
package esp8266

import (
	"fmt"
	"path"
	"strconv"

	log "github.com/Sirupsen/logrus"
	"github.com/resin-io/edge-node-manager/radio/wifi"
)

type Esp8266 struct {
	Log       *log.Logger
	LocalUUID string
}

func (b Esp8266) InitialiseRadio() error {
	return wifi.Initialise()
}

func (b Esp8266) CleanupRadio() error {
	return wifi.Cleanup()
}

func (b Esp8266) Update(filePath string) error {
	b.Log.Info("Starting update")

	ip, err := wifi.GetIP(b.LocalUUID)
	if err != nil {
		return err
	}

	if err := wifi.PostForm("http://"+ip+"/update", path.Join(filePath, "firmware.bin")); err != nil {
		return err
	}

	b.Log.Info("Finished update")

	return nil
}

func (b Esp8266) Scan(applicationUUID int) (map[string]struct{}, error) {
	return wifi.Scan(strconv.Itoa(applicationUUID))
}

func (b Esp8266) Online() (bool, error) {
	return wifi.Online(b.LocalUUID)
}

func (b Esp8266) Restart() error {
	b.Log.Info("Restarting...")
	return fmt.Errorf("Restart not implemented")
}

func (b Esp8266) Identify() error {
	b.Log.Info("Identifying...")
	return fmt.Errorf("Identify not implemented")
}

func (b Esp8266) UpdateConfig(config interface{}) error {
	b.Log.WithFields(log.Fields{
		"Config": config,
	}).Info("Updating config...")
	return fmt.Errorf("Update config not implemented")
}

func (b Esp8266) UpdateEnvironment(config interface{}) error {
	b.Log.WithFields(log.Fields{
		"Config": config,
	}).Info("Updating environment...")
	return fmt.Errorf("Update environment not implemented")
}
<file_sep>/config/config.go
package config

import (
	"os"
	"path"
	"strconv"
	"time"

	log "github.com/Sirupsen/logrus"
)

// GetLogLevel returns the log level
func GetLogLevel() log.Level {
	return switchLogLevel(getEnv("ENM_LOG_LEVEL", ""))
}

// GetDependentLogLevel returns the log level for dependent devices
func GetDependentLogLevel() log.Level {
	return switchLogLevel(getEnv("DEPENDENT_LOG_LEVEL", ""))
}

// GetSupervisorCheckDelay returns the time delay in seconds between each supervisor check at startup
func GetSupervisorCheckDelay() (time.Duration, error) {
	value, err := strconv.Atoi(getEnv("ENM_SUPERVISOR_CHECK_DELAY", "1"))
	return time.Duration(value) * time.Second, err
}

// GetHotspotDeleteDelay returns the time delay in seconds between hotspot deletion and creation
func GetHotspotDeleteDelay() (time.Duration, error) {
	value, err := strconv.Atoi(getEnv("ENM_HOTSPOT_DELETE_DELAY", "10"))
	return time.Duration(value) * time.Second, err
}

// GetLoopDelay returns the time delay in seconds between each application process loop
func GetLoopDelay() (time.Duration, error) {
	value, err := strconv.Atoi(getEnv("ENM_CONFIG_LOOP_DELAY", "10"))
	return time.Duration(value) * time.Second, err
}

// GetPauseDelay returns the time delay in seconds between each pause check
func GetPauseDelay() (time.Duration, error) {
	value, err := strconv.Atoi(getEnv("ENM_CONFIG_PAUSE_DELAY", "10"))
	return time.Duration(value) * time.Second, err
}

// GetHotspotSSID returns the SSID to be used for the hotspot
func GetHotspotSSID() string {
	return getEnv("ENM_HOTSPOT_SSID", "resin-hotspot")
}

// GetHotspotPassword returns the password to be used for the hotspot
func GetHotspotPassword() string {
	return getEnv("ENM_HOTSPOT_PASSWORD", "<PASSWORD>")
}

// GetShortBluetoothTimeout returns the timeout for each instantaneous bluetooth operation
func GetShortBluetoothTimeout() (time.Duration, error) {
	value, err := strconv.Atoi(getEnv("ENM_BLUETOOTH_SHORT_TIMEOUT", "1"))
	return time.Duration(value) * time.Second, err
}

// GetLongBluetoothTimeout returns the timeout for each long running bluetooth operation
func GetLongBluetoothTimeout() (time.Duration, error) {
	value, err := strconv.Atoi(getEnv("ENM_BLUETOOTH_LONG_TIMEOUT", "10"))
	return time.Duration(value) * time.Second, err
}

// GetAvahiTimeout returns the timeout for each Avahi scan operation
func GetAvahiTimeout() (time.Duration, error) {
	value, err := strconv.Atoi(getEnv("ENM_AVAHI_TIMEOUT", "10"))
	return time.Duration(value) * time.Second, err
}

// GetUpdateRetries returns the number of times the firmware update process should be attempted
func GetUpdateRetries() (int, error) {
	return strconv.Atoi(getEnv("ENM_UPDATE_RETRIES", "1"))
}

// GetAssetsDir returns the root directory used to store the database and application commits
func GetAssetsDir() string {
	return getEnv("ENM_ASSETS_DIRECTORY", "/data/assets")
}

// GetDbDir returns the directory used to store the database
func GetDbDir() string {
	return getEnv("ENM_DB_DIRECTORY", "/data/database")
}

// GetDbPath returns the path used to store the database
func GetDbPath() string {
	directory := GetDbDir()
	file := getEnv("ENM_DB_FILE", "enm.db")

	return path.Join(directory, file)
}

// GetVersion returns the API version used to communicate with the supervisor
func GetVersion() string {
	return getEnv("ENM_API_VERSION", "v1")
}

// GetSuperAddr returns the address used to communicate with the supervisor
func GetSuperAddr() string {
	return getEnv("RESIN_SUPERVISOR_ADDRESS", "http://127.0.0.1:4000")
}

// GetSuperAPIKey returns the API key used to communicate with the supervisor
func GetSuperAPIKey() string {
	return getEnv("RESIN_SUPERVISOR_API_KEY", "")
}

// GetLockFileLocation returns the location of the lock file
func GetLockFileLocation() string {
	return getEnv("ENM_LOCK_FILE_LOCATION", "/tmp/resin/resin-updates.lock")
}

func getEnv(key, fallback string) string {
	result := os.Getenv(key)
	if result == "" {
		result = fallback
	}
	return result
}

func switchLogLevel(level string) log.Level {
	switch level {
	case "Debug":
		return log.DebugLevel
	case "Info":
		return log.InfoLevel
	case "Warn":
		return log.WarnLevel
	case "Error":
		return log.ErrorLevel
	case "Fatal":
		return log.FatalLevel
	case "Panic":
		return log.PanicLevel
	}

	return log.InfoLevel
}
<file_sep>/main.go
package main

import (
	"encoding/json"
	"net/http"
	"os"
	"sort"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/asdine/storm"
	"github.com/jmoiron/jsonq"
	"github.com/resin-io/edge-node-manager/api"
	"github.com/resin-io/edge-node-manager/application"
	"github.com/resin-io/edge-node-manager/config"
	"github.com/resin-io/edge-node-manager/device"
	"github.com/resin-io/edge-node-manager/process"
	"github.com/resin-io/edge-node-manager/supervisor"
)

var (
	// This variable will be populated at build time with the current version tag
	version string
	// This variable defines the delay between each processing loop
	loopDelay time.Duration
)

func main() {
	log.Info("Starting edge-node-manager")

	if err := checkVersion(); err != nil {
		log.Error("Unable to check if edge-node-manager is up to date")
	}

	supervisor.WaitUntilReady()

	for {
		// Run processing loop
		loop()

		// Delay between processing each set of applications to prevent 100% CPU usage
		time.Sleep(loopDelay)
	}
}

func init() {
	log.SetLevel(config.GetLogLevel())
	log.SetFormatter(&log.TextFormatter{ForceColors: true, DisableTimestamp: true})

	var err error
	loopDelay, err = config.GetLoopDelay()
	if err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to load loop delay")
	}

	dbDir := config.GetDbDir()
	if err := os.MkdirAll(dbDir, os.ModePerm); err != nil {
		log.WithFields(log.Fields{
			"Directory": dbDir,
			"Error":     err,
		}).Fatal("Unable to create database directory")
	}

	db, err := storm.Open(config.GetDbPath())
	if err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to open database")
	}
	defer db.Close()

	if err := db.Init(&device.Device{}); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to initialise database")
	}

	go func() {
		router := api.NewRouter()
		port := ":8081"

		log.WithFields(log.Fields{
			"Port": port,
		}).Debug("Initialising incoming supervisor API")

		if err := http.ListenAndServe(port, router); err != nil {
			log.WithFields(log.Fields{
				"Error": err,
			}).Fatal("Unable to initialise incoming supervisor API")
		}
	}()
}

func checkVersion() error {
	resp, err := http.Get("https://api.github.com/repos/resin-io/edge-node-manager/releases/latest")
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	data := map[string]interface{}{}
	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
		return err
	}

	latest, err := jsonq.NewQuery(data).String("tag_name")
	if err != nil {
		return err
	}

	if version == latest {
		log.WithFields(log.Fields{
			"Current version": version,
		}).Info("edge-node-manager upto date")
	} else {
		log.WithFields(log.Fields{
			"Current version": version,
			"Latest version":  latest,
			"Update command":  "git push resin master:resin-nocache",
		}).Warn("Please update edge-node-manager")
	}

	return nil
}

func loop() {
	// Get applications from the supervisor
	bytes, errs := supervisor.DependentApplicationsList()
	if errs != nil {
		log.WithFields(log.Fields{
			"Errors": errs,
		}).Error("Unable to get applications")
		return
	}

	// Unmarshal applications
	applications, err := application.Unmarshal(bytes)
	if err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to unmarshal applications")
		return
	}

	// Sort applications to ensure they run in order
	var keys []int
	for key := range applications {
		keys = append(keys, key)
	}
	sort.Ints(keys)

	// Process applications
	for _, key := range keys {
		if errs := process.Run(applications[key]); errs != nil {
			log.WithFields(log.Fields{
				"Application": applications[key],
				"Errors":      errs,
			}).Error("Unable to process application")
		}
	}
}
<file_sep>/CHANGELOG.md
# Change Log

All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).

## v3.0.0 - 2018-03-27

* Update device name parameter to comply with API v4 and supervisor v7 #199 [Lucian Buzzo]

## v2.4.0 - 2017-09-08

* Configure time between hotspot deletion/creation #193 [Joe Roberts]

## v2.3.0 - 2017-09-08

* Configure time between supervisor checks #194 [Joe Roberts]

## v2.2.0 - 2017-08-29

* Improve device endpoints #190 [Joe Roberts]

## v2.1.0 - 2017-08-18

* Implement query dependent device endpoint #187 [Joe Roberts]

## v2.0.1 - 2017-08-18

* Check for index-out-of-range #186 [Joe Roberts]

## v2.0.0 - 2017-08-14

* Implement avahi/mdns discovery #185 [Joe Roberts]
* Document config variables in the readme #185 [Joe Roberts]

## v1.6.2 - 2017-07-10

* Updated readme [Joe Roberts]

## v1.6.1 - 2017-07-10

* Implement context timeout for nmap scan [Joe Roberts]

## v1.6.0 - 2017-07-06

* Tag architectures with the correct name [Joe Roberts]

## v1.5.3 - 2017-07-06

* Only perform extra bluetooth initialisation if the device type is raspberrypi [Joe Roberts]

## v1.5.2 - 2017-07-06

* Another attempt to fix conflicts between jobs [Joe Roberts]

## v1.5.1 - 2017-07-06

* Fix issue with conflicting travis jobs [Joe Roberts]
* Fix typo in variable check [Joe Roberts]

## v1.5.0 - 2017-07-06

* Get architecture during the docker build [Joe Roberts]

## v1.4.5 - 2017-05-19

* Add more ignores to .gitignore [<NAME>]

## v1.4.4 - 2017-05-19

* Update `versionist` config to latest, ensuring semver types are case insensitive. [Heds Simons]

## v1.4.3 - 2017-04-25

* Refactor hotspot creation [Joseph Roberts]
* Removed unused resin-hotspot system connection [Joseph Roberts]
* Update dependencies [Joseph Roberts]

## v1.4.2 - 2017-04-21

* Ignore db open errors allowing the supervisor to silently retry [Joseph Roberts]

## v1.4.1 - 2017-04-21

* Improve radio initialisation [Joseph Roberts]

## v1.4.0 - 2017-04-07

* ESP Support [Joseph Roberts]

## v1.3.0 - 2017-03-23

* Remove state from memory by writing to database [Joseph Roberts]

## v1.2.1 - 2017-03-16

* Wrap timeout around all bluetooth operations [Joseph Roberts]

## v1.2.0 - 2017-03-16

* Implement git auto release [Joseph Roberts]

## v1.1.0 - 2017-03-15

* Implement out of date warning [Joseph Roberts]

## v1.0.3 - 2017-03-15

* Remove edge-node-manager binary
* Add edge-node-manager to .gitignore [Joseph Roberts]

## v1.0.2 - 2017-03-13

* Sync changelog with release [Joseph Roberts]

## v1.0.1 - 2017-03-10

* Get latest tag from the git API
* Add update lock info to readme

## v1.0.0 - 2017-03-10

* Set up versionist
* Implement lock mechanism
* Defer save state
* Increased time to allow device to disconnect
* Fix att-request-failed error
* Quick hacky temporary fix to solve slow DFU
* Remove disconnect requests as the dep. device disconnects on its own
* Replace wget with curl
* Temp fix device status by sending every minute
* Retry updates
* Bluetooth stability fixes
* Add dep device log level
* Removed timestamp so that the supervisor will set the timestamp instead

## v0.1.9 2017-03-01

* Updated travis icon
* Fixed nrf51822dk issue and added some debugging
* Removed bluez apt source

## v0.1.8 2017-02-23

* Updated readme
* Updated dockerfile
* Micro:bit now restarts after an update
* Removed trailing new lines from log output
* Changed default log level to Info
* Updated gitignore
* Removed images

## v0.1.7 2017-02-21

* Removed resin sync from repo
* Refactored bluetooth
* Auto build binaries when a tag is created on master

## v0.1.6 2016-11-28

* Merge pull request #86 from resin-io/develop

## v0.1.5 2016-11-01

* Set up flag and logic to ensure is_online is always sent first time (#50)
* 17 send progress dep device logs (#58)
* Enforce order (#59)
* Tidied logging (#60)
* Turned off notifications
* Fixing dep. device delete (#61)
* 36 dep device env and config (#62)
* 63 pull down fields (#64)
* 36 dep device env and config (#65)

## v0.1.4 2016-10-26

* Fixed device deletion (#44)
* Auto board type (#45)
* Auto board type (#46)

## v0.1.3 2016-10-25

* Merge pluginification into develop
* Custom device name issue#35 (#37)
* Quick online state bug fix
* Another quick bug fix
* Microbit (#40)

## v0.1.2 2016-10-18

* Merge develop into master

## v0.1.1 2016-10-11

* Merge develop into master

## v0.1.0 2016-10-04

* Updated git ignore
* Merge initial-architecture into develop
* Merge bluetooth-radio into develop
* Merge godoc-comments into develop
* Merge linting into develop
* Merge proxyvisor-integration into develop
* Merge docker into develop
* Bumped docker version to 1.7
* Refactored build method slightly
* Merge testing into develop
<file_sep>/api/routes.go
package api

import "net/http"

// Route contains all the variables needed to define a route
type Route struct {
	Name        string
	Method      string
	Pattern     string
	HandlerFunc http.HandlerFunc
}

// Routes holds all the routes assigned to the API
type Routes []Route

var routes = Routes{
	Route{
		"DependentDeviceUpdate",
		"PUT",
		"/v1/devices/{uuid}",
		DependentDeviceUpdate,
	},
	Route{
		"DependentDeviceDelete",
		"DELETE",
		"/v1/devices/{uuid}",
		DependentDeviceDelete,
	},
	Route{
		"DependentDeviceRestart",
		"PUT",
		"/v1/devices/{uuid}/restart",
		DependentDeviceRestart,
	},
	Route{
		"DependentDevicesQuery",
		"GET",
		"/v1/devices",
		DependentDevicesQuery,
	},
	Route{
		"DependentDeviceQuery",
		"GET",
		"/v1/devices/{uuid}",
		DependentDeviceQuery,
	},
	Route{
		"SetStatus",
		"PUT",
		"/v1/enm/status",
		SetStatus,
	},
	Route{
		"GetStatus",
		"GET",
		"/v1/enm/status",
		GetStatus,
	},
}
<file_sep>/application/application.go
package application

import (
	"encoding/json"
	"fmt"

	"github.com/resin-io/edge-node-manager/board"
	"github.com/resin-io/edge-node-manager/board/esp8266"
	"github.com/resin-io/edge-node-manager/board/microbit"
	"github.com/resin-io/edge-node-manager/board/nrf51822dk"
)

type Application struct {
	Board        board.Interface        `json:"-"`
	BoardType    board.Type             `json:"-"`
	Name         string                 `json:"name"`
	ResinUUID    int                    `json:"id"`
	Commit       string                 `json:"-"`      // Ignore this when unmarshalling from the supervisor as we want to set the target commit
	TargetCommit string                 `json:"commit"` // Set json tag to commit as the supervisor has no concept of target commit
	Config       map[string]interface{} `json:"config"`
}

func (a Application) String() string {
	return fmt.Sprintf(
		"Board type: %s, "+
			"Name: %s, "+
			"Resin UUID: %d, "+
			"Commit: %s, "+
			"Target commit: %s, "+
			"Config: %v",
		a.BoardType,
		a.Name,
		a.ResinUUID,
		a.Commit,
		a.TargetCommit,
		a.Config)
}

func Unmarshal(bytes []byte) (map[int]Application, error) {
	applications := make(map[int]Application)

	var buffer []Application
	if err := json.Unmarshal(bytes, &buffer); err != nil {
		return nil, err
	}

	for key, value := range buffer {
		value, ok := value.Config["RESIN_HOST_TYPE"]
		if !ok {
			continue
		}
		boardType := (board.Type)(value.(string))

		var b board.Interface
		switch boardType {
		case board.MICROBIT:
			b = microbit.Microbit{}
		case board.NRF51822DK:
			b = nrf51822dk.Nrf51822dk{}
		case board.ESP8266:
			b = esp8266.Esp8266{}
		default:
			continue
		}

		application := buffer[key]
		application.BoardType = boardType
		application.Board = b

		applications[application.ResinUUID] = application
	}

	return applications, nil
}
<file_sep>/radio/wifi/nm.go
package wifi

import (
	"fmt"

	"github.com/godbus/dbus"
)

type NmDeviceState uint32

const (
	NmDeviceStateUnknown      NmDeviceState = 0
	NmDeviceStateUnmanaged    NmDeviceState = 10
	NmDeviceStateUnavailable  NmDeviceState = 20
	NmDeviceStateDisconnected NmDeviceState = 30
	NmDeviceStatePrepare      NmDeviceState = 40
	NmDeviceStateConfig       NmDeviceState = 50
	NmDeviceStateNeed_auth    NmDeviceState = 60
	NmDeviceStateIp_config    NmDeviceState = 70
	NmDeviceStateIp_check     NmDeviceState = 80
	NmDeviceStateSecondaries  NmDeviceState = 90
	NmDeviceStateActivated    NmDeviceState = 100
	NmDeviceStateDeactivating NmDeviceState = 110
	NmDeviceStateFailed       NmDeviceState = 120
)

type NmDeviceType uint32

const (
	NmDeviceTypeUnknown    NmDeviceType = 0
	NmDeviceTypeEthernet   NmDeviceType = 1
	NmDeviceTypeWifi       NmDeviceType = 2
	NmDeviceTypeUnused1    NmDeviceType = 3
	NmDeviceTypeUnused2    NmDeviceType = 4
	NmDeviceTypeBt         NmDeviceType = 5
	NmDeviceTypeOlpcMesh   NmDeviceType = 6
	NmDeviceTypeWimax      NmDeviceType = 7
	NmDeviceTypeModem      NmDeviceType = 8
	NmDeviceTypeInfiniband NmDeviceType = 9
	NmDeviceTypeBond       NmDeviceType = 10
	NmDeviceTypeVlan       NmDeviceType = 11
	NmDeviceTypeAdsl       NmDeviceType = 12
	NmDeviceTypeBridge     NmDeviceType = 13
	NmDeviceTypeGeneric    NmDeviceType = 14
	NmDeviceTypeTeam       NmDeviceType = 15
)

type NmActiveConnectionState uint32

const (
	NmActiveConnectionStateUnknown      NmActiveConnectionState = 0
	NmActiveConnectionStateActivating   NmActiveConnectionState = 1
	NmActiveConnectionStateActivated    NmActiveConnectionState = 2
	NmActiveConnectionStateDeactivating NmActiveConnectionState = 3
	NmActiveConnectionStateDeactivated  NmActiveConnectionState = 4
)

type NmDevice struct {
	nmPath      dbus.ObjectPath
	nmState     NmDeviceState
	nmType      NmDeviceType
	nmInterface string
}

func removeHotspotConnections(ssid string) error {
	settingsObject, err := getConnection(ssid)
	if err != nil {
		return err
	} else if settingsObject == nil {
		return nil
	}

	if err := settingsObject.Call("org.freedesktop.NetworkManager.Settings.Connection.Delete", 0).Store(); err != nil {
		return err
	}

	for {
		if settingsObject, err := getConnection(ssid); err != nil {
			return err
		} else if settingsObject == nil {
			break
		}
	}

	return nil
}

func getConnection(ssid string) (dbus.BusObject, error) {
	connection, err := dbus.SystemBus()
	if err != nil {
		return nil, err
	}

	var settingsPaths []dbus.ObjectPath
	settingsObject := connection.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings")
	if err := settingsObject.Call("org.freedesktop.NetworkManager.Settings.ListConnections", 0).Store(&settingsPaths); err != nil {
		return nil, err
	}

	for _, settingsPath := range settingsPaths {
		var settings map[string]map[string]dbus.Variant
		settingsObject := connection.Object("org.freedesktop.NetworkManager", settingsPath)
		if err := settingsObject.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&settings); err != nil {
			return nil, err
		}

		if settings["connection"]["id"].Value().(string) == ssid {
			return settingsObject, nil
		}
	}

	return nil, nil
}

func isEthernetConnected() (bool, error) {
	devices, err := getDevices()
	if err != nil {
		return false, err
	}

	for _, device := range devices {
		if device.nmType == NmDeviceTypeEthernet && device.nmState == NmDeviceStateActivated {
			return true, nil
		}
	}

	return false, nil
}

func getWifiDevice() (NmDevice, error) {
	devices, err := getDevices()
	if err != nil {
		return NmDevice{}, err
	}

	for _, device := range devices {
		if device.nmType == NmDeviceTypeWifi {
			return device, nil
		}
	}

	return NmDevice{}, fmt.Errorf("No wifi device found")
}

func getFreeWifiDevice() (NmDevice, error) {
	devices, err := getDevices()
	if err != nil {
		return NmDevice{}, err
	}

	for _, device := range devices {
		if device.nmType == NmDeviceTypeWifi && device.nmState == NmDeviceStateDisconnected {
			return device, nil
		}
	}

	return NmDevice{}, fmt.Errorf("No free wifi device found")
}

func createHotspotConnection(device NmDevice, ssid, password string) error {
	connection, err := dbus.SystemBus()
	if err != nil {
		return err
	}

	hotspot := make(map[string]map[string]interface{})

	hotspot["802-11-wireless"] = make(map[string]interface{})
	hotspot["802-11-wireless"]["band"] = "bg"
	hotspot["802-11-wireless"]["hidden"] = false
	hotspot["802-11-wireless"]["mode"] = "ap"
	hotspot["802-11-wireless"]["security"] = "802-11-wireless-security"
	hotspot["802-11-wireless"]["ssid"] = []byte(ssid)

	hotspot["802-11-wireless-security"] = make(map[string]interface{})
	hotspot["802-11-wireless-security"]["key-mgmt"] = "wpa-psk"
	hotspot["802-11-wireless-security"]["psk"] = password

	hotspot["connection"] = make(map[string]interface{})
	hotspot["connection"]["autoconnect"] = false
	hotspot["connection"]["id"] = ssid
	hotspot["connection"]["interface-name"] = device.nmInterface
	hotspot["connection"]["type"] = "801-11-wireless"

	hotspot["ipv4"] = make(map[string]interface{})
	hotspot["ipv4"]["method"] = "shared"

	var path, activeConnectionPath dbus.ObjectPath
	rootObject := connection.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
	if err := rootObject.Call(
		"org.freedesktop.NetworkManager.AddAndActivateConnection",
		0,
		hotspot,
		device.nmPath,
		dbus.ObjectPath("/")).
		Store(&path, &activeConnectionPath); err != nil {
		return err
	}

	activeConnectionObject := connection.Object("org.freedesktop.NetworkManager", activeConnectionPath)
	for {
		value, err := getProperty(activeConnectionObject, "org.freedesktop.NetworkManager.Connection.Active.State")
		if err != nil {
			return err
		}

		if NmActiveConnectionState(value.(uint32)) == NmActiveConnectionStateActivated {
			break
		}
	}

	return nil
}

func getDevices() ([]NmDevice, error) {
	connection, err := dbus.SystemBus()
	if err != nil {
		return nil, err
	}

	var paths []dbus.ObjectPath
	rootObject := connection.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
	if err := rootObject.Call("org.freedesktop.NetworkManager.GetAllDevices", 0).Store(&paths); err != nil {
		return nil, err
	}

	devices := make([]NmDevice, 5)
	for _, path := range paths {
		deviceObject := connection.Object("org.freedesktop.NetworkManager", path)

		device := NmDevice{}
		device.nmPath = path

		value, err := getProperty(deviceObject, "org.freedesktop.NetworkManager.Device.State")
		if err != nil {
			return nil, err
		}
		device.nmState = NmDeviceState(value.(uint32))

		value, err = getProperty(deviceObject, "org.freedesktop.NetworkManager.Device.DeviceType")
		if err != nil {
			return nil, err
		}
		device.nmType = NmDeviceType(value.(uint32))

		value, err = getProperty(deviceObject, "org.freedesktop.NetworkManager.Device.Interface")
		if err != nil {
			return nil, err
		}
		device.nmInterface = value.(string)

		devices = append(devices, device)
	}

	return devices, nil
}

func getProperty(object dbus.BusObject, property string) (interface{}, error) {
	value, err := object.GetProperty(property)
	if err != nil {
		return nil, err
	}

	return value.Value(), nil
}
<file_sep>/process/process.go
package process

import (
	"os"
	"path"
	"strconv"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/asdine/storm"
	"github.com/asdine/storm/index"
	"github.com/asdine/storm/q"
	"github.com/fredli74/lockfile"
	"github.com/resin-io/edge-node-manager/application"
	"github.com/resin-io/edge-node-manager/config"
	"github.com/resin-io/edge-node-manager/device"
	deviceStatus "github.com/resin-io/edge-node-manager/device/status"
	processStatus "github.com/resin-io/edge-node-manager/process/status"
	"github.com/resin-io/edge-node-manager/supervisor"
	tarinator "github.com/verybluebot/tarinator-go"
)

var (
	CurrentStatus processStatus.Status
	TargetStatus  processStatus.Status
	updateRetries int
	pauseDelay    time.Duration
	lockLocation  string
	lock          *lockfile.LockFile
)

func Run(a application.Application) []error {
	log.Info("----------------------------------------")

	// Pause the process if necessary
	if err := pause(); err != nil {
		return []error{err}
	}

	// Initialise the radio
	if err := a.Board.InitialiseRadio(); err != nil {
		return []error{err}
	}
	defer a.Board.CleanupRadio()

	if log.GetLevel() == log.DebugLevel {
		log.WithFields(log.Fields{
			"Application": a,
		}).Debug("Processing application")
	} else {
		log.WithFields(log.Fields{
			"Application": a.Name,
		}).Info("Processing application")
	}

	// Enable update locking
	var err error
	lock, err = lockfile.Lock(lockLocation)
	if err != nil {
		return []error{err}
	}
	defer lock.Unlock()

	// Handle delete flags
	if err := handleDelete(a); err != nil {
		return []error{err}
	}

	// Get all online devices associated with this application
	onlineDevices, err := getOnlineDevices(a)
	if err != nil {
		return []error{err}
	}

	// Get all provisioned devices associated with this application
	provisionedDevices, err := getProvisionedDevices(a)
	if err != nil {
		return []error{err}
	}

	if log.GetLevel() == log.DebugLevel {
		log.WithFields(log.Fields{
			"Provisioned devices": provisionedDevices,
		}).Debug("Processing application")
	} else {
		log.WithFields(log.Fields{
			"Number of provisioned devices": len(provisionedDevices),
		}).Info("Processing application")
	}

	// Convert provisioned devices to a hash map
	hashmap := make(map[string]struct{})
	var s struct{}
	for _, value := range provisionedDevices {
		hashmap[value.LocalUUID] = s
	}

	// Provision all unprovisioned devices associated with this application
	for key := range onlineDevices {
		if _, ok := hashmap[key]; ok {
			// Device already provisioned
			continue
		}

		// Device not already provisioned
		if errs := provisionDevice(a, key); errs != nil {
			return errs
		}
	}

	// Refesh all provisioned devices associated with this application
	provisionedDevices, err = getProvisionedDevices(a)
	if err != nil {
		return []error{err}
	}

	// Sync all provisioned devices associated with this application
	for _, value := range provisionedDevices {
		if errs := value.Sync(); errs != nil {
			return errs
		}

		if err := updateDevice(value); err != nil {
			return []error{err}
		}
	}

	// Refesh all provisioned devices associated with this application
	provisionedDevices, err = getProvisionedDevices(a)
	if err != nil {
		return []error{err}
	}

	// Set state for all provisioned devices associated with this application
	for _, value := range provisionedDevices {
		if _, ok := onlineDevices[value.LocalUUID]; ok {
			value.Status = deviceStatus.IDLE
		} else {
			value.Status = deviceStatus.OFFLINE
		}

		if err := updateDevice(value); err != nil {
			return []error{err}
		}

		if errs := sendState(value); errs != nil {
			return errs
		}
	}

	// Refesh all provisioned devices associated with this application
	provisionedDevices, err = getProvisionedDevices(a)
	if err != nil {
		return []error{err}
	}

	// Update all online, outdated, provisioned devices associated with this application
	for _, value := range provisionedDevices {
		if (value.Commit != value.TargetCommit) && (value.Status != deviceStatus.OFFLINE) {
			// Populate board (and micro) for the device
			if err := value.PopulateBoard(); err != nil {
				return []error{err}
			}

			// Perform the update
			if errs := updateFirmware(value); errs != nil {
				return errs
			}
		}
	}

	return nil
}

func init() {
	log.SetLevel(config.GetLogLevel())

	var err error
	if pauseDelay, err = config.GetPauseDelay(); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to load pause delay")
	}

	if updateRetries, err = config.GetUpdateRetries(); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to update retries")
	}

	lockLocation = config.GetLockFileLocation()

	CurrentStatus = processStatus.RUNNING
	TargetStatus = processStatus.RUNNING

	log.Debug("Initialised process")
}

func pause() error {
	if TargetStatus != processStatus.PAUSED {
		return nil
	}

	CurrentStatus = processStatus.PAUSED
	log.WithFields(log.Fields{
		"Status": CurrentStatus,
	}).Info("Process status")

	for TargetStatus == processStatus.PAUSED {
		time.Sleep(pauseDelay)
	}

	CurrentStatus = processStatus.RUNNING
	log.WithFields(log.Fields{
		"Status": CurrentStatus,
	}).Info("Process status")

	return nil
}

func getOnlineDevices(a application.Application) (map[string]struct{}, error) {
	onlineDevices, err := a.Board.Scan(a.ResinUUID)
	if err != nil {
		return nil, err
	}

	log.WithFields(log.Fields{
		"Number of online devices": len(onlineDevices),
	}).Info("Processing application")

	return onlineDevices, nil
}

func getProvisionedDevices(a application.Application) ([]device.Device, error) {
	db, err := storm.Open(config.GetDbPath())
	if err != nil {
		return nil, err
	}
	defer db.Close()

	var provisionedDevices []device.Device
	if err := db.Find("ApplicationUUID", a.ResinUUID, &provisionedDevices); err != nil && err.Error() != index.ErrNotFound.Error() {
		return nil, err
	}

	return provisionedDevices, nil
}

func provisionDevice(a application.Application, localUUID string) []error {
	log.WithFields(log.Fields{
		"Local UUID": localUUID,
	}).Info("Provisioning device")

	resinUUID, name, errs := supervisor.DependentDeviceProvision(a.ResinUUID)
	if errs != nil {
		return errs
	}

	db, err := storm.Open(config.GetDbPath())
	if err != nil {
		return []error{err}
	}
	defer db.Close()

	d := device.New(a.ResinUUID, a.BoardType, name, localUUID, resinUUID)
	if err := db.Save(&d); err != nil {
		return []error{err}
	}

	log.WithFields(log.Fields{
		"Name":       d.Name,
		"Local UUID": d.LocalUUID,
	}).Info("Provisioned device")

	return nil
}

func updateDevice(d device.Device) error {
	db, err := storm.Open(config.GetDbPath())
	if err != nil {
		return err
	}
	defer db.Close()

	return db.Update(&d)
}

func sendState(d device.Device) []error {
	online := true
	if d.Status == deviceStatus.OFFLINE {
		online = false
	}

	return supervisor.DependentDeviceInfoUpdateWithOnlineState(d.ResinUUID, (string)(d.Status), d.Commit, online)
}

func updateFirmware(d device.Device) []error {
	online, err := d.Board.Online()
	if err != nil {
		return []error{err}
	} else if !online {
		return nil
	}

	filepath, err := getFirmware(d)
	if err != nil {
		return []error{err}
	}

	d.Status = deviceStatus.INSTALLING
	if err := updateDevice(d); err != nil {
		return []error{err}
	}
	if errs := sendState(d); errs != nil {
		return errs
	}

	for i := 1; i <= updateRetries; i++ {
		log.WithFields(log.Fields{
			"Name":    d.Name,
			"Attempt": i,
		}).Info("Starting update")

		if err := d.Board.Update(filepath); err != nil {
			log.WithFields(log.Fields{
				"Name":  d.Name,
				"Error": err,
			}).Error("Update failed")
			continue
		} else {
			log.WithFields(log.Fields{
				"Name": d.Name,
			}).Info("Finished update")
			d.Commit = d.TargetCommit
			break
		}
	}

	d.Status = deviceStatus.IDLE
	if err := updateDevice(d); err != nil {
		return []error{err}
	}
	return sendState(d)
}

func getFirmware(d device.Device) (string, error) {
	// Build the file paths
	filepath := config.GetAssetsDir()
	filepath = path.Join(filepath, strconv.Itoa(d.ApplicationUUID))
	filepath = path.Join(filepath, d.TargetCommit)
	tarPath := path.Join(filepath, "binary.tar")

	// Check if the firmware exists
	if _, err := os.Stat(tarPath); os.IsNotExist(err) {
		// Download the firmware
		if err := supervisor.DependentApplicationUpdate(d.ApplicationUUID, d.TargetCommit); err != nil {
			return "", err
		}

		// Extract the firmware
		if err := tarinator.UnTarinate(filepath, tarPath); err != nil {
			return "", err
		}
	}

	return filepath, nil
}

func handleDelete(a application.Application) error {
	db, err := storm.Open(config.GetDbPath())
	if err != nil {
		return err
	}
	defer db.Close()

	if err := db.Select(q.Eq("ApplicationUUID", a.ResinUUID), q.Eq("DeleteFlag", true)).Delete(&device.Device{}); err != nil && err.Error() != index.ErrNotFound.Error() {
		return err
	}

	return nil
}
<file_sep>/board/board.go
package board

type Type string

const (
	MICROBIT   Type = "microbit"
	NRF51822DK      = "nrf51822dk"
	ESP8266         = "esp8266"
)

type Interface interface {
	InitialiseRadio() error
	CleanupRadio() error
	Update(filePath string) error
	Scan(applicationUUID int) (map[string]struct{}, error)
	Online() (bool, error)
	Restart() error
	Identify() error
	UpdateConfig(interface{}) error
	UpdateEnvironment(interface{}) error
}
<file_sep>/process/status/status.go
package status

// Status defines the process statuses
type Status string

const (
	RUNNING Status = "Running"
	PAUSED         = "Paused"
)
<file_sep>/api/handlers.go
package api

import (
	"encoding/json"
	"net/http"

	"github.com/asdine/storm"
	"github.com/asdine/storm/q"
	"github.com/gorilla/mux"
	"github.com/resin-io/edge-node-manager/config"
	"github.com/resin-io/edge-node-manager/device"
	"github.com/resin-io/edge-node-manager/process"
	"github.com/resin-io/edge-node-manager/process/status"

	log "github.com/Sirupsen/logrus"
)

func DependentDeviceUpdate(w http.ResponseWriter, r *http.Request) {
	type dependentDeviceUpdate struct {
		Commit      string      `json:"commit"`
		Environment interface{} `json:"environment"`
	}

	decoder := json.NewDecoder(r.Body)
	var content dependentDeviceUpdate
	if err := decoder.Decode(&content); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to decode Dependent device update hook")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	if err := setField(r, "TargetCommit", content.Commit); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusAccepted)
}

func DependentDeviceDelete(w http.ResponseWriter, r *http.Request) {
	if err := setField(r, "Delete", true); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
}

func DependentDeviceRestart(w http.ResponseWriter, r *http.Request) {
	if err := setField(r, "Restart", true); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
}

func DependentDevicesQuery(w http.ResponseWriter, r *http.Request) {
	db, err := storm.Open(config.GetDbPath())
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	defer db.Close()

	var d []device.Device
	if err := db.All(&d); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to find devices in database")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	bytes, err := json.Marshal(d)
	if err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to encode devices")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	if written, err := w.Write(bytes); (err != nil) || (written != len(bytes)) {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to write response")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	log.Debug("Get dependent device")
}

func DependentDeviceQuery(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	UUID := vars["uuid"]

	db, err := storm.Open(config.GetDbPath())
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	defer db.Close()

	var d device.Device
	if err := db.Select(
		q.Or(
			q.Eq("LocalUUID", UUID),
			q.Eq("ResinUUID", UUID),
		),
	).First(&d); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
			"UUID":  UUID,
		}).Error("Unable to find device in database")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	bytes, err := json.Marshal(d)
	if err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to encode device")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	if written, err := w.Write(bytes); (err != nil) || (written != len(bytes)) {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to write response")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	log.WithFields(log.Fields{
		"Device": d,
	}).Debug("Get dependent device")
}

func SetStatus(w http.ResponseWriter, r *http.Request) {
	type s struct {
		TargetStatus status.Status `json:"targetStatus"`
	}

	var content *s
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&content); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to decode status hook")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	process.TargetStatus = content.TargetStatus

	w.WriteHeader(http.StatusOK)

	log.WithFields(log.Fields{
		"Target status": process.TargetStatus,
	}).Debug("Set status")
}

func GetStatus(w http.ResponseWriter, r *http.Request) {
	type s struct {
		CurrentStatus status.Status `json:"currentStatus"`
		TargetStatus  status.Status `json:"targetStatus"`
	}

	content := &s{
		CurrentStatus: process.CurrentStatus,
		TargetStatus:  process.TargetStatus,
	}

	bytes, err := json.Marshal(content)
	if err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to encode status hook")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	if written, err := w.Write(bytes); (err != nil) || (written != len(bytes)) {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to write response")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	log.WithFields(log.Fields{
		"Target status": process.TargetStatus,
		"Curent status": process.CurrentStatus,
	}).Debug("Get status")
}

func setField(r *http.Request, key string, value interface{}) error {
	vars := mux.Vars(r)
	deviceUUID := vars["uuid"]

	db, err := storm.Open(config.GetDbPath())
	if err != nil {
		return err
	}
	defer db.Close()

	var d device.Device
	if err := db.One("ResinUUID", deviceUUID, &d); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
			"UUID":  deviceUUID,
		}).Error("Unable to find device in database")
		return err
	}

	switch key {
	case "TargetCommit":
		d.TargetCommit = value.(string)
	case "Delete":
		d.DeleteFlag = value.(bool)
	case "Restart":
		d.RestartFlag = value.(bool)
	default:
		log.WithFields(log.Fields{
			"Error": err,
			"UUID":  deviceUUID,
			"Key":   key,
			"value": value,
		}).Error("Unable to set field")
		return err
	}

	if err := db.Update(&d); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
			"UUID":  deviceUUID,
		}).Error("Unable to update device in database")
		return err
	}

	log.WithFields(log.Fields{
		"UUID":  deviceUUID,
		"Key":   key,
		"value": value,
	}).Debug("Dependent device field updated")

	return nil
}
<file_sep>/release.sh
#!/bin/bash

set -o errexit

if [ -z "$ACCOUNT" ] || [ -z "$REPO" ] || [ -z "$ACCESS_TOKEN" ] || [ -z "$TRAVIS_TAG" ]; then
	echo "Please set value for ACCOUNT, REPO, ACCESS_TOKEN and TRAVIS_TAG"
	exit 1
fi

echo "Attempting to create a new $TRAVIS_TAG release"
json="{
	\"tag_name\": \"$TRAVIS_TAG\",
	\"name\": \"$TRAVIS_TAG\",
	\"body\": \"Release of $TRAVIS_TAG: [changelog](https://github.com/resin-io/edge-node-manager/blob/master/CHANGELOG.md)\n$1\"
}"

resp=$(curl -i --data "$json" --header "Content-Type:application/json" \
	"https://api.github.com/repos/$ACCOUNT/$REPO/releases?access_token=$ACCESS_TOKEN" | \
	head -n 1 | cut -d$' ' -f2)

if [ $resp = "201" ]; then
	echo "Success"
elif [ $resp = "422" ]; then
	echo "Release already exists, appending instead"

	release=$(curl https://api.github.com/repos/$ACCOUNT/$REPO/releases/tags/$TRAVIS_TAG)
	id=$(echo $release | jq .id)
	body=$(echo $release | jq .body)
	body="${body%\"}"
	body="${body#\"}"

	json="{
		\"body\": \"$body\n$1\"
	}"

	resp=$(curl --data "$json" --header "Content-Type:application/json" \
		-X PATCH "https://api.github.com/repos/$ACCOUNT/$REPO/releases/$id?access_token=$ACCESS_TOKEN" | \
		head -n 1 | cut -d$' ' -f2)

	if [ $resp = "200" ]; then
		exit 0
	else
		exit 1
	fi
fi
<file_sep>/device/status/status.go
package status

// Status defines the device statuses
type Status string

const (
	DOWNLOADING Status = "Downloading"
	INSTALLING         = "Installing"
	STARTING           = "Starting"
	STOPPING           = "Stopping"
	IDLE               = "Idle"
	OFFLINE            = "Offline"
)
<file_sep>/board/microbit/microbit.go
package microbit

import (
	"fmt"
	"strconv"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/currantlabs/ble"
	"github.com/resin-io/edge-node-manager/config"
	"github.com/resin-io/edge-node-manager/micro/nrf51822"
	"github.com/resin-io/edge-node-manager/radio/bluetooth"
)

type Microbit struct {
	Log   *log.Logger
	Micro nrf51822.Nrf51822
}

var (
	dfu          *ble.Characteristic
	shortTimeout time.Duration
)

func (b Microbit) InitialiseRadio() error {
	return b.Micro.InitialiseRadio()
}

func (b Microbit) CleanupRadio() error {
	return b.Micro.CleanupRadio()
}

func (b Microbit) Update(filePath string) error {
	b.Log.Info("Starting update")

	if err := b.Micro.ExtractFirmware(filePath, "micro-bit.bin", "micro-bit.dat"); err != nil {
		return err
	}

	name, err := bluetooth.GetName(b.Micro.LocalUUID)
	if err != nil {
		return err
	}

	if name != "DfuTarg" {
		b.Log.Debug("Starting bootloader")

		client, err := bluetooth.Connect(b.Micro.LocalUUID)
		if err != nil {
			return err
		}

		// Ignore the error because this command causes the device to disconnect
		bluetooth.WriteCharacteristic(client, dfu, []byte{nrf51822.Start}, false)

		// Give the device time to disconnect
		time.Sleep(shortTimeout)

		b.Log.Debug("Started bootloader")
	} else {
		b.Log.Debug("Bootloader already started")
	}

	client, err := bluetooth.Connect(b.Micro.LocalUUID)
	if err != nil {
		return err
	}

	if err := b.Micro.Update(client); err != nil {
		return err
	}

	b.Log.Info("Finished update")

	return nil
}

func (b Microbit) Scan(applicationUUID int) (map[string]struct{}, error) {
	id := "BBC micro:bit [" + strconv.Itoa(applicationUUID) + "]"
	return bluetooth.Scan(id)
}

func (b Microbit) Online() (bool, error) {
	return bluetooth.Online(b.Micro.LocalUUID)
}

func (b Microbit) Restart() error {
	b.Log.Info("Restarting...")
	return fmt.Errorf("Restart not implemented")
}

func (b Microbit) Identify() error {
	b.Log.Info("Identifying...")
	return fmt.Errorf("Identify not implemented")
}

func (b Microbit) UpdateConfig(config interface{}) error {
	b.Log.WithFields(log.Fields{
		"Config": config,
	}).Info("Updating config...")
	return fmt.Errorf("Update config not implemented")
}

func (b Microbit) UpdateEnvironment(config interface{}) error {
	b.Log.WithFields(log.Fields{
		"Config": config,
	}).Info("Updating environment...")
	return fmt.Errorf("Update environment not implemented")
}

func init() {
	log.SetLevel(config.GetLogLevel())

	var err error
	if shortTimeout, err = config.GetShortBluetoothTimeout(); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to load bluetooth timeout")
	}

	dfu, err = bluetooth.GetCharacteristic("e95d93b1251d470aa062fa1922dfa9a8", ble.CharRead+ble.CharWrite, 0x0D, 0x0E)
	if err != nil {
		log.Fatal(err)
	}

	log.Debug("Initialised micro:bit characteristics")
}
<file_sep>/radio/wifi/wifi.go
package wifi

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/grandcat/zeroconf"
	"github.com/parnurzeal/gorequest"
	"github.com/resin-io/edge-node-manager/config"
)

var (
	initialised  bool
	avahiTimeout time.Duration
)

type Host struct {
	ip              string
	deviceType      string
	applicationUUID string
	id              string
}

func Initialise() error {
	if initialised {
		return nil
	}

	log.Info("Initialising wifi hotspot")

	os.Setenv("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/host/run/dbus/system_bus_socket")

	ssid := config.GetHotspotSSID()
	password := config.GetHotspotPassword()

	if err := removeHotspotConnections(ssid); err != nil {
		return err
	}

	if delay, err := config.GetHotspotDeleteDelay(); err != nil {
		return err
	} else {
		time.Sleep(delay)
	}

	// If ethernet is connected, create the hotspot on the first wifi interface found
	// If ethernet is not connected, create the hotspot on the first FREE wifi interface found
	var device NmDevice
	if ethernet, err := isEthernetConnected(); err != nil {
		return err
	} else if ethernet {
		if device, err = getWifiDevice(); err != nil {
			return err
		}
	} else {
		if device, err = getFreeWifiDevice(); err != nil {
			return err
		}
	}

	if err := createHotspotConnection(device, ssid, password); err != nil {
		return err
	}

	log.WithFields(log.Fields{
		"SSID":     ssid,
		"Password": <PASSWORD>,
		"Device":   device,
	}).Info("Initialised wifi hotspot")

	initialised = true
	return nil
}

func Cleanup() error {
	// Return as we do not want to disable the hotspot
	return nil
}

func Scan(id string) (map[string]struct{}, error) {
	hosts, err := scan()
	if err != nil {
		return nil, err
	}

	online := make(map[string]struct{})
	for _, host := range hosts {
		if host.applicationUUID == id {
			var s struct{}
			online[host.id] = s
		}
	}

	return online, nil
}

func Online(id string) (bool, error) {
	hosts, err := scan()
	if err != nil {
		return false, err
	}

	for _, host := range hosts {
		if host.id == id {
			return true, nil
		}
	}

	return false, nil
}

func GetIP(id string) (string, error) {
	hosts, err := scan()
	if err != nil {
		return "", err
	}

	for _, host := range hosts {
		if host.id == id {
			return host.ip, nil
		}
	}

	return "", fmt.Errorf("Device offline")
}

func PostForm(url, filePath string) error {
	req := gorequest.New()
	req.Post(url)
	req.Type("multipart")
	req.SendFile(filePath, "firmware.bin", "image")

	log.WithFields(log.Fields{
		"URL":    req.Url,
		"Method": req.Method,
	}).Info("Posting form")

	resp, _, errs := req.End()
	return handleResp(resp, errs, http.StatusOK)
}

func init() {
	log.SetLevel(config.GetLogLevel())

	var err error
	if avahiTimeout, err = config.GetAvahiTimeout(); err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Fatal("Unable to load Avahi timeout")
	}

	log.Debug("Initialised wifi")
}

func scan() ([]Host, error) {
	ctx, cancel := context.WithTimeout(context.Background(), avahiTimeout)
	defer cancel()

	resolver, err := zeroconf.NewResolver(nil)
	if err != nil {
		return nil, err
	}

	entries := make(chan *zeroconf.ServiceEntry)
	var hosts []Host
	go func(entries <-chan *zeroconf.ServiceEntry, hosts *[]Host) {
		for entry := range entries {
			parts := strings.Split(entry.ServiceRecord.Instance, "_")

			if len(entry.AddrIPv4) < 1 || len(parts) < 3 {
				continue
			}

			host := Host{
				ip:              entry.AddrIPv4[0].String(),
				deviceType:      parts[0],
				applicationUUID: parts[1],
				id:              parts[2],
			}
			*hosts = append(*hosts, host)
		}
	}(entries, &hosts)

	err = resolver.Browse(ctx, "_http._tcp", "local", entries)
	if err != nil {
		log.WithFields(log.Fields{
			"Error": err,
		}).Error("Unable to scan")
		return nil, err
	}

	<-ctx.Done()

	return hosts, nil
}

func handleResp(resp gorequest.Response, errs []error, statusCode int) error {
	if errs != nil {
		return errs[0]
	}

	if resp.StatusCode != statusCode {
		return fmt.Errorf("Invalid response received: %s", resp.Status)
	}

	log.WithFields(log.Fields{
		"Response": resp.Status,
	}).Debug("Valid response received")

	return nil
}
<file_sep>/device/hook/hook.go
package hook

import (
	"io/ioutil"
	"regexp"

	"github.com/Sirupsen/logrus"
	"github.com/resin-io/edge-node-manager/config"
	"github.com/resin-io/edge-node-manager/supervisor"
)

type Hook struct {
	ResinUUID string
}

func (h *Hook) Fire(entry *logrus.Entry) error {
	serialised, _ := entry.Logger.Formatter.Format(entry)
	message := regexp.MustCompile(`\r?\n`).ReplaceAllString((string)(serialised), "")
	supervisor.DependentDeviceLog(h.ResinUUID, message)

	return nil
}

func (h *Hook) Levels() []logrus.Level {
	return []logrus.Level{
		logrus.PanicLevel,
		logrus.FatalLevel,
		logrus.ErrorLevel,
		logrus.WarnLevel,
		logrus.InfoLevel,
		logrus.DebugLevel,
	}
}

func Create(resinUUID string) *logrus.Logger {
	log := logrus.New()
	log.Out = ioutil.Discard
	log.Level = config.GetDependentLogLevel()
	log.Formatter = &logrus.TextFormatter{ForceColors: true, DisableTimestamp: true}
	log.Hooks.Add(&Hook{
		ResinUUID: resinUUID,
	})

	return log
}
