func refreshMeshInfo() (*meshInfo, error) { nodes, err := getNodes() if err != nil { return nil, err } sites, err := getSites() if err != nil { return nil, err } node, ok := nodes[config.Serial()] if !ok { return nil, errors.New("Could not find our node in the cloud. (race condition?)") } site, ok := sites[node.SiteID] if !ok { return nil, errors.New("Could not find our node in the cloud. (race condition?)") } if config.Bool(false, "forceMaster") { site.MasterNodeID = config.Serial() } meshInfo := &meshInfo{ SiteID: site.ID, MasterNodeID: site.MasterNodeID, SiteUpdated: int(time.Time(site.Updated).UnixNano() / int64(time.Second)), } return meshInfo, saveMeshInfo(meshInfo) }
// bridgeMqtt connects one mqtt broker to another. Shouldn't probably be doing this. But whatever. func (c *client) bridgeMqtt(from, to bus.Bus, masterToSlave bool, topics []string) { onMessage := func(topic string, payload []byte) { if masterToSlave { // This is a message from master, clear the timeout c.masterReceiveTimeout.Reset(orphanTimeout) } if payload[0] != '{' { log.Warningf("Invalid payload (should be a json-rpc object): %s", payload) return } var msg meshMessage json.Unmarshal(payload, &msg) interesting := false if masterToSlave { // Interesting if it's from the master or one of the other slaves interesting = msg.Source == nil || (*msg.Source != config.Serial()) } else { // Interesting if it's from me interesting = msg.Source == nil } log.Infof("Mesh master2slave:%t topic:%s interesting:%t", masterToSlave, topic, interesting) if interesting { if msg.Source == nil { if masterToSlave { payload = addMeshSource(config.MustString("masterNodeId"), payload) } else { payload = addMeshSource(config.Serial(), payload) } } to.Publish(topic, payload) } } for _, topic := range topics { _, err := from.Subscribe(topic, onMessage) if err != nil { log.Fatalf("Failed to subscribe to topic %s when bridging to master: %s", topic, err) } } }
func runTasks() { // Find this sphere's location, if we care.. if sameRoomOnly { for _, thing := range allThings { if thing.Type == "node" && thing.Device != nil && thing.Device.NaturalID == config.Serial() { if thing.Location != nil && (roomID == nil || *roomID != *thing.Location) { // Got it. log.Infof("Got this sphere's location: %s", thing.Location) roomID = thing.Location select { case foundLocation <- true: default: } break } } } } for _, task := range tasks { go func(t *request) { t.cb(getChannelServices(t.thingType, t.protocol, t.filter)) }(task) } }
// ExportDriver Exports a driver using the 'driver' protocol, and announces it func (c *Connection) ExportDriver(driver Driver) error { time.Sleep(config.Duration(time.Second*3, "drivers.startUpDelay")) topic := fmt.Sprintf("$node/%s/driver/%s", config.Serial(), driver.GetModuleInfo().ID) announcement := driver.GetModuleInfo() announcement.ServiceAnnouncement = model.ServiceAnnouncement{ Schema: "http://schema.ninjablocks.com/service/driver", } _, err := c.exportService(driver, topic, announcement) if err != nil { return err } if config.Bool(false, "autostart") { err := c.GetServiceClient(topic).Call("start", struct{}{}, nil, time.Second*20) if err != nil { c.log.Fatalf("Failed to autostart driver: %s", err) } } return nil }
// ExportApp Exports an app using the 'app' protocol, and announces it func (c *Connection) ExportApp(app App) error { if app.GetModuleInfo().ID == "" { panic("You must provide an ID in the package.json") } topic := fmt.Sprintf("$node/%s/app/%s", config.Serial(), app.GetModuleInfo().ID) announcement := app.GetModuleInfo() announcement.ServiceAnnouncement = model.ServiceAnnouncement{ Schema: "http://schema.ninjablocks.com/service/app", } _, err := c.exportService(app, topic, announcement) if err != nil { return err } if config.Bool(false, "autostart") { err := c.GetServiceClient(topic).Call("start", struct{}{}, nil, time.Second*20) if err != nil { c.log.Fatalf("Failed to autostart app: %s", err) } } return nil }
func (c *HomeCloud) AutoStartModules() { do := func(name string, task string) error { return c.Conn.SendNotification("$node/"+config.Serial()+"/module/"+task, name) } interval := config.MustDuration("homecloud.autoStart.interval") for _, name := range c.findAutoStartModules() { log.Infof("-- (Re)starting '%s'", name) err := do(name, "stop") if err != nil { log.Fatalf("Failed to send %s stop message! %s", name, err) } time.Sleep(interval) err = do(name, "start") if err != nil { log.Fatalf("Failed to send %s start message! %s", name, err) } } }
func NewSystemPane(conn *ninja.Connection) Pane { pane := &SystemPane{ log: logger.GetLogger("SystemPane"), code: "0000", color: "green", } status := conn.GetServiceClient("$device/:deviceId/component/:componentId") status.OnEvent("status", func(statusEvent *StatusEvent, values map[string]string) bool { if deviceId, ok := values["deviceId"]; !ok { return true } else if deviceId != config.Serial() { return true } else { params, _ := json.Marshal(statusEvent) pane.log.Infof("$device/%s/component/%s - %s", deviceId, values["componentId"], params) pane.code = statusEvent.Code pane.color = statusEvent.Color if pane.color == "" { pane.color = "green" } return true } }) return pane }
func activate(client *http.Client, url string) (*credentials, error) { log.Debugf("Requesting url: %s", url) resp, err := client.Get(url) if err != nil { return nil, err } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode == http.StatusRequestTimeout { return nil, nil } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("Failed to activate: %s - %s", resp.Status, body) } log.Debugf("Got response: %s", body) var response nodeClaimResponse err = json.Unmarshal(body, &response) if err != nil { log.Fatalf("Failed to unmarshal credentials from cloud: %s (%s)", body, err) } if response.Data.NodeID != config.Serial() { log.Fatalf("Incorrect node id returned from pairing! Expected %s got %s", config.Serial(), response.Data.NodeID) } if response.Data.UserID == "" || response.Data.Token == "" || response.Data.SphereNetworkKey == "" { log.Fatalf("Invalid credentials (missing value): %s", body) } return &credentials{ UserID: response.Data.UserID, Token: response.Data.Token, SphereNetworkKey: response.Data.SphereNetworkKey, }, err }
func (d *NodeDevice) GetDeviceInfo() *model.Device { if d.modelDevice == nil { name := "Spheramid " + config.Serial() sphereVersion := config.SphereVersion() d.modelDevice = &model.Device{ NaturalID: config.Serial(), NaturalIDType: "node", Name: &name, Signatures: &map[string]string{ "ninja:manufacturer": "Ninja Blocks Inc.", "ninja:productName": "Spheramid", "ninja:thingType": "node", "ninja:sphereVersion": sphereVersion, }, } } return d.modelDevice }
func NewLedController(conn *ninja.Connection) (*LedController, error) { s, err := util.GetLEDConnection() if err != nil { log.Fatalf("Failed to get connection to LED matrix: %s", err) } // Send a blank image to the led matrix util.WriteLEDMatrix(image.NewRGBA(image.Rect(0, 0, 16, 16)), s) controller := &LedController{ conn: conn, pairingLayout: ui.NewPairingLayout(), serial: s, waiting: make(chan bool), } conn.MustExportService(controller, "$node/"+config.Serial()+"/led-controller", &model.ServiceAnnouncement{ Schema: "/service/led-controller", }) conn.MustExportService(controller, "$home/led-controller", &model.ServiceAnnouncement{ Schema: "/service/led-controller", }) if config.HasString("siteId") { log.Infof("Have a siteId, checking if homecloud is running") // If we have just started, and we have a site, and homecloud is running... enable control! go func() { siteModel := conn.GetServiceClient("$home/services/SiteModel") for { if controller.commandReceived { log.Infof("Command has been received, stopping search for homecloud.") break } err := siteModel.Call("fetch", config.MustString("siteId"), nil, time.Second*5) if err != nil { log.Infof("Fetched site to enableControl. Got err: %s", err) } else if err == nil && !controller.commandReceived { controller.EnableControl() break } time.Sleep(time.Second * 5) } }() } return controller, nil }
func showPane() { led := driver.conn.GetServiceClient("$node/" + config.Serial() + "/led-controller") fmt.Println(led) err := led.Call("displayIcon", ledmodel.IconRequest{ Icon: "orphaned.gif", }, nil, 0) if err != nil { fmt.Println("========") fmt.Println(err) fmt.Println("========") } }
func (c *client) bridgeToMaster(host net.IP, port int) { log.Debugf("Bridging to the master: %s:%d", host, port) mqttURL := fmt.Sprintf("%s:%d", host, port) clientID := "slave-" + config.Serial() log.Infof("Connecting to master %s using cid:%s", mqttURL, clientID) c.masterBus = bus.MustConnect(mqttURL, clientID) c.localBus = bus.MustConnect(fmt.Sprintf("%s:%d", config.MustString("mqtt.host"), config.MustInt("mqtt.port")), "meshing") log.Infof("Connected to master? %t", c.masterBus.Connected()) if c.masterBus.Connected() { c.setUnorphaned() } else { c.setOrphaned() } c.masterBus.OnDisconnect(func() { log.Infof("Disconnected from master") go func() { time.Sleep(time.Second * 5) if !c.masterBus.Connected() { log.Infof("Still disconnected from master, setting orphaned.") c.setOrphaned() } }() }) c.masterBus.OnConnect(func() { log.Infof("Connected to master") go func() { time.Sleep(time.Second * 2) if !c.masterBus.Connected() { log.Infof("Still connected to master, setting unorphaned") c.setUnorphaned() } }() }) bridgeTopics := []string{"$discover", "$site/#", "$home/#" /*deprecated*/, "$node/#", "$thing/#", "$device/#"} c.bridgeMqtt(c.masterBus, c.localBus, true, bridgeTopics) c.bridgeMqtt(c.localBus, c.masterBus, false, bridgeTopics) }
func Start() { log.Infof("Starting client on Node: %s", config.Serial()) conn, err := ninja.Connect("client") if err != nil { log.Fatalf("Failed to connect to sphere: %s", err) } client := &client{ conn: conn, led: conn.GetServiceClient("$home/led-controller"), foundMaster: make(chan bool), } if !config.IsPaired() { err = UpdateSphereAvahiService(false, false) if err != nil { log.Fatalf("Failed to update avahi service: %s", err) } } client.updatePairingLight("black", false) conn.SubscribeRaw("$sphere/bridge/status", client.onBridgeStatus) client.start() log.Infof("Client started.") err = UpdateSphereAvahiService(true, client.master) if err != nil { log.Fatalf("Failed to update avahi service: %s", err) } if runtime.GOOS == "linux" { go func() { err := client.ensureTimezoneIsSet() if err != nil { log.Warningf("Could not save timezone: %s", err) } }() } listenToSiteUpdates(conn) }
func (c *client) pair() error { var boardType string if config.HasString("boardType") { boardType = config.MustString("boardType") } else { boardType = fmt.Sprintf("custom-%s-%s", runtime.GOOS, runtime.GOARCH) } log.Debugf("Board type: %s", boardType) client := &http.Client{ Timeout: time.Second * 60, // It's 20sec on the server so this *should* be ok } if config.Bool(false, "cloud", "allowSelfSigned") { log.Warningf("Allowing self-signed certificate (should only be used to connect to development cloud)") client.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } var creds *credentials for { url := fmt.Sprintf(config.MustString("cloud", "activation"), config.Serial(), getLocalIP(), boardType) log.Debugf("Activating at URL: %s", url) var err error creds, err = activate(client, url) if err != nil { log.Warningf("Activation error : %s", err) log.Warningf("Sleeping for 3sec") time.Sleep(time.Second * 3) } else if creds != nil { break } } log.Infof("Got credentials. User: %s", creds.UserID) return saveCreds(creds) }
func UpdateSphereAvahiService(isPaired, isMaster bool) error { tmpl, err := template.New("avahi").Parse(src) if err != nil { return err } serviceDefinition := new(bytes.Buffer) err = tmpl.Execute(serviceDefinition, map[string]interface{}{ "Serial": config.Serial(), "Master": isMaster, "Paired": isPaired, "User": config.String("", "userId"), "Site": config.String("", "siteId"), "MasterNode": config.String("", "masterNodeId"), "SiteUpdated": config.Int(0, "siteUpdated"), "RestPort": config.MustInt("homecloud.rest.port"), }) if err != nil { return err } log.Debugf("Saving service definition", serviceDefinition.String()) if runtime.GOOS != "linux" { log.Warningf("Avahi service definition is not being saved, as platform != linux") return nil } if _, err := os.Stat("/data/etc/avahi/services"); err != nil { log.Warningf("Avahi service definition is not being saved, as /data/etc/avahi/services does not exist") return nil } err = ioutil.WriteFile("/data/etc/avahi/services/ninjasphere.service", []byte(serviceDefinition.String()), 0644) // HACK: Remove this if it doesn't fix Chris's problem exec.Command("service", "avahi-daemon", "restart").Output() return err }
func getUser(driver *HueDriver, bridge *hue.Bridge) *hue.User { var user *hue.User var err error noUser := true retries := 0 serial := config.Serial() username := serial + serial //username must be long 10-40 characters isvaliduser, err := bridge.IsValidUser(username) if err != nil { log.Warningf("Problem determining if hue user is valid") } var notificationTime time.Time if isvaliduser { user = hue.NewUserWithBridge(username, bridge) } else { for noUser { user, err = bridge.CreateUser("ninjadevice", username) if err != nil { if strings.Contains(err.Error(), "101") { // there's probably a nicer way to check this retries++ log.Debugf("Couldn't make user, push link button. Retry: %d", retries) if time.Since(notificationTime) > time.Minute*5 { notificationTime = time.Now() driver.sendEvent("notification", pushButtonNotification) } time.Sleep(time.Second * 2) //this sucks } else { log.Debugf("Error creating user %s", err) time.Sleep(time.Second * 20) } } if user != nil { noUser = false } } } return user }
func NewLight(driver *Driver, D int, name string, port *arduino.Arduino) error { light, err := devices.CreateLightDevice(driver, &model.Device{ NaturalID: fmt.Sprintf("%s-%d", config.Serial(), D), NaturalIDType: "block-arduino", Name: &name, Signatures: &map[string]string{ "ninja:manufacturer": "Ninja Blocks Inc", "ninja:productType": "Light", "ninja:thingType": "light", }, }, driver.Conn) if err != nil { log.FatalError(err, "Could not create light device") } if err := light.EnableOnOffChannel(); err != nil { log.FatalError(err, "Could not enable hue on-off channel") } if err := light.EnableBrightnessChannel(); err != nil { log.FatalError(err, "Could not enable hue brightness channel") } if err := light.EnableColorChannel("temperature", "hue"); err != nil { log.FatalError(err, "Could not enable color channel") } port.OnDeviceData(func(data arduino.DeviceData) { if data.D == D { spew.Dump("Light Data!", data) } }) return nil }
func (b *TinyBus) connect() { b.connecting.Add(1) defer func() { b.connecting.Done() b.publish(&proto.Publish{ Header: proto.Header{ Retain: true, }, TopicName: fmt.Sprintf("node/%s/module/%s/state/connected", config.Serial(), b.id), Payload: proto.BytesPayload([]byte("true")), }) }() var conn wrappedConn for { tcpConn, err := net.Dial("tcp", b.host) if err == nil { conn = wrappedConn{ Conn: tcpConn, done: make(chan bool, 1), } break } //log.Warningf("Failed to connect to: %s", err) time.Sleep(time.Millisecond * 500) } if b.mqtt != nil { log.Infof("Reconnected to mqtt server") } mqtt := mqtt.NewClientConn(conn) mqtt.ClientId = b.id err := mqtt.ConnectCustom(&proto.Connect{ WillFlag: true, WillQos: 0, WillRetain: true, WillTopic: fmt.Sprintf("$node/%s/module/%s/state/connected", config.Serial(), b.id), WillMessage: "false", }) if err != nil { log.Fatalf("MQTT Failed to connect to: %s", err) } b.mqtt = mqtt b.connected() for _, s := range b.subscriptions { if !s.cancelled { b.subscribe(s) } } go func() { for m := range mqtt.Incoming { b.onIncoming(m) } }() go func() { <-conn.done b.disconnected() if !b.destroyed { b.connect() } }() }
package rest import ( "encoding/json" "io/ioutil" "net/http" "github.com/ninjasphere/go-ninja/config" "github.com/ninjasphere/go-ninja/logger" ) var wrapped = true var log = logger.GetLogger("HomeCloud.Router") // doesn't change so we cache it. var NodeID = config.Serial() // ResponseWrapper used to wrap responses from the API type ResponseWrapper struct { Type string `json:"type,omitempty"` Data interface{} `json:"data,omitempty"` } // WriteServerErrorResponse Builds the wrapped error for the client func WriteServerErrorResponse(msg string, code int, w http.ResponseWriter) { body, err := json.Marshal(&ResponseWrapper{Type: "error", Data: msg}) if err != nil { log.Errorf("Unable to serialise response: %s", err) }
func (c *client) start() { if !config.IsPaired() { log.Infof("Client is unpaired. Attempting to pair.") if err := c.pair(); err != nil { log.Fatalf("An error occurred while pairing. Restarting. error: %s", err) } log.Infof("Pairing was successful.") // We reload the config so the creds can be picked up config.MustRefresh() if !config.IsPaired() { log.Fatalf("Pairing appeared successful, but I did not get the credentials. Restarting.") } } log.Infof("Client is paired. User: %s", config.MustString("userId")) if !config.NoCloud() { mesh, err := refreshMeshInfo() if err == errorUnauthorised { log.Warningf("UNAUTHORISED! Unpairing.") c.unpair() return } if err != nil { log.Warningf("Failed to refresh mesh info: %s", err) } else { log.Debugf("Got mesh info: %+v", mesh) } config.MustRefresh() if !config.HasString("masterNodeId") { log.Warningf("We don't have any mesh information. Which is unlikely. But we can't do anything without it, so restarting client.") time.Sleep(time.Second * 10) os.Exit(0) } } if config.MustString("masterNodeId") == config.Serial() { log.Infof("I am the master, starting HomeCloud.") cmd := exec.Command("start", "sphere-homecloud") cmd.Output() go c.exportNodeDevice() c.master = true } else { log.Infof("I am a slave. The master is %s", config.MustString("masterNodeId")) // TODO: Remove this when we are running drivers on slaves cmd := exec.Command("stop", "sphere-director") cmd.Output() c.masterReceiveTimeout = time.AfterFunc(orphanTimeout, func() { c.setOrphaned() }) } go func() { log.Infof("Starting search for peers") for { c.findPeers() time.Sleep(time.Second * 30) } }() }
func (c *client) findPeers() { query := "_ninja-homecloud-mqtt._tcp" // Make a channel for results and start listening entriesCh := make(chan *mdns.ServiceEntry, 4) go func() { for entry := range entriesCh { if !strings.Contains(entry.Name, query) { continue } nodeInfo := parseMdnsInfo(entry.Info) id, ok := nodeInfo["ninja.sphere.node_id"] if !ok { log.Warningf("Found a node, but couldn't get it's node id. %v", entry) continue } if id == config.Serial() { // It's me. continue } user, ok := nodeInfo["ninja.sphere.user_id"] if !ok { log.Warningf("Found a node, but couldn't get it's user id. %v", entry) continue } site, ok := nodeInfo["ninja.sphere.site_id"] siteUpdated, ok := nodeInfo["ninja.sphere.site_updated"] masterNodeID, ok := nodeInfo["ninja.sphere.master_node_id"] if user == config.MustString("userId") { if site == config.MustString("siteId") { log.Infof("Found a sibling node (%s) - %s", id, entry.Addr) siteUpdatedInt, err := strconv.ParseInt(siteUpdated, 10, 64) if err != nil { log.Warningf("Failed to read the site_updated field (%s) on node %s - %s", siteUpdated, id, entry.Addr) } else { if int(siteUpdatedInt) > config.MustInt("siteUpdated") { log.Infof("Found node (%s - %s) with a newer site update time (%s).", id, entry.Addr, siteUpdated) info := &meshInfo{ MasterNodeID: masterNodeID, SiteID: config.MustString("siteId"), SiteUpdated: int(siteUpdatedInt), } err := saveMeshInfo(info) if err != nil { log.Warningf("Failed to save updated mesh info from node: %s - %+v", err, info) } if masterNodeID == config.MustString("masterNodeId") { log.Infof("Updated master id is the same (%s). Moving on with our lives.", masterNodeID) } else { log.Infof("Master id has changed (was %s now %s). Rebooting", config.MustString("masterNodeId"), masterNodeID) reboot() return } } } } else { log.Warningf("Found a node owned by the same user (%s) but from a different site (%s) - ID:%s - %s", user, site, id, entry.Addr) } } else { log.Infof("Found a node owned by another user (%s) (%s) - %s", user, id, entry.Addr) } if id == config.MustString("masterNodeId") { log.Infof("Found the master node (%s) - %s", id, entry.Addr) select { case c.foundMaster <- true: default: } if !c.bridged { c.bridgeToMaster(entry.Addr, entry.Port) c.bridged = true c.exportNodeDevice() } } } }() // Start the lookup mdns.Lookup(query, entriesCh) close(entriesCh) }
func (c *client) unpair() { log.Infof("Unpairing") c.conn.SendNotification(fmt.Sprintf("$node/%s/unpair", config.Serial()), nil) }