// Connect Builds a new ninja connection to the MQTT broker, using the given client ID func Connect(clientID string) (*Connection, error) { log := logger.GetLogger(fmt.Sprintf("%s.connection", clientID)) conn := Connection{ log: log, services: []model.ServiceAnnouncement{}, } mqttURL := fmt.Sprintf("%s:%d", config.MustString("mqtt", "host"), config.MustInt("mqtt", "port")) log.Infof("Connecting to %s using cid:%s", mqttURL, clientID) conn.mqtt = bus.MustConnect(mqttURL, clientID) log.Infof("Connected") conn.rpc = rpc.NewClient(conn.mqtt, json2.NewClientCodec()) conn.rpcServer = rpc.NewServer(conn.mqtt, json2.NewCodec()) // Add service discovery service. Responds to queries about services exposed in this process. discoveryService := &discoverService{&conn} _, err := conn.exportService(discoveryService, "$discover", &simpleService{*discoveryService.GetServiceAnnouncement()}) if err != nil { log.Fatalf("Could not expose discovery service: %s", err) } return &conn, nil }
func (c *client) exportNodeDevice() { if c.nodeDevice != nil { return } c.nodeDevice = &NodeDevice{info: ninja.LoadModuleInfo("./package.json")} // TODO: Make some generic way to see if homecloud is running. // XXX: Fix this. It's ugly. for { siteModel := c.conn.GetServiceClient("$home/services/SiteModel") err := siteModel.Call("fetch", config.MustString("siteId"), nil, time.Second*5) if err == nil { break } log.Infof("Failed to fetch siteid from sitemodel: %s", err) time.Sleep(time.Second * 5) } for { err := c.conn.ExportDevice(c.nodeDevice) if err == nil { break } log.Warningf("Failed to export node device. Retrying in 5 sec: %s", err) time.Sleep(time.Second * 5) } }
func (c *client) ensureTimezoneIsSet() error { siteModel := c.conn.GetServiceClient("$home/services/SiteModel") var site model.Site for { err := siteModel.Call("fetch", config.MustString("siteId"), &site, time.Second*5) if err == nil && site.TimeZoneID != nil { log.Infof("Saving timezone: %s", *site.TimeZoneID) cmd := exec.Command("with-rw", "ln", "-s", "-f", "/usr/share/zoneinfo/"+*site.TimeZoneID, "/etc/localtime") _, err := cmd.Output() if err != nil { return err } break } time.Sleep(time.Second * 2) } return nil }
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 (m *SiteModel) Delete(id string, conn redis.Conn) error { m.syncing.Wait() //defer m.sync() if id == "here" { id = config.MustString("siteId") } return m.delete(id, conn) }
func (p *WeatherPane) GetWeather() { enableWeatherPane = false for { site := &model.Site{} err := p.siteModel.Call("fetch", config.MustString("siteId"), site, time.Second*5) if err == nil && (site.Longitude != nil || site.Latitude != nil) { p.site = site globalSite = site if site.TimeZoneID != nil { if timezone, err = time.LoadLocation(*site.TimeZoneID); err != nil { log.Warningf("error while setting timezone (%s): %s", *site.TimeZoneID, err) timezone, _ = time.LoadLocation("Local") } } break } log.Infof("Failed to get site, or site has no location.") time.Sleep(time.Second * 2) } for { p.weather.DailyByCoordinates( &owm.Coordinates{ Longitude: *p.site.Longitude, Latitude: *p.site.Latitude, }, 1, ) if len(p.weather.List) > 0 { filename := util.ResolveImagePath("weather/" + p.weather.List[0].Weather[0].Icon + ".png") if _, err := os.Stat(filename); os.IsNotExist(err) { enableWeatherPane = false fmt.Printf("Couldn't load image for weather: %s", filename) bugsnag.Notify(fmt.Errorf("Unknown weather icon: %s", filename), p.weather) } else { p.image = util.LoadImage(filename) enableWeatherPane = true } } time.Sleep(weatherUpdateInterval) } }
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 getNodes() (map[string]Node, error) { var data []Node err := req(config.MustString("cloud", "nodes"), &data) log.Debugf("Fetched nodes: %+v", data) m := make(map[string]Node) for _, n := range data { m[n.ID] = n } return m, err }
func getSites() (map[string]Site, error) { var data []Site err := req(config.MustString("cloud", "sites"), &data) log.Debugf("Fetched sites: %+v", data) m := make(map[string]Site) for _, s := range data { m[s.ID] = s } return m, err }
// 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 NewConfigService(scheduler *service.SchedulerService, conn *ninja.Connection) *ConfigService { siteID := config.MustString("siteId") service := &ConfigService{ scheduler: scheduler, thingModel: conn.GetServiceClient("$home/services/ThingModel"), roomModel: conn.GetServiceClient("$home/services/RoomModel"), siteModel: conn.GetServiceClient("$home/services/SiteModel"), presets: conn.GetServiceClient(fmt.Sprintf("$site/%s/service/presets", siteID)), rooms: make(map[string]*nmodel.Room), sites: make(map[string]*nmodel.Site), } return service }
func (a *presetsAction) actuate(ctx *actuationContext) error { siteID := config.MustString("siteId") topic := fmt.Sprintf("$site/%s/service/%s", siteID, "presets") client := ctx.conn.GetServiceClient(topic) id := a.getModel().GetSceneID() if id != "" { params := []string{id} reply := &struct{}{} return client.Call(a.model.Action, params, reply, ctx.timeout) } else { return fmt.Errorf("The scene id for presets action was empty. The actuation did nothing.") } }
func (c *client) onBridgeStatus(status *bridgeStatus) bool { log.Debugf("Got bridge status. connected:%t configured:%t", status.Connected, status.Configured) if status.Connected { c.updatePairingLight("green", false) } else { if c.master { c.updatePairingLight("red", true) } else { c.updatePairingLight("blue", false) } } if !status.Configured && c.master { log.Infof("Configuring bridge") c.conn.PublishRawSingleValue("$sphere/bridge/connect", map[string]string{ "url": config.MustString("cloud", "url"), "token": config.MustString("token"), }) } return true }
func (m *SiteModel) Fetch(id string, conn redis.Conn) (*model.Site, error) { m.syncing.Wait() if id == "here" { id = config.MustString("siteId") } site := &model.Site{} if err := m.fetch(id, site, false, conn); err != nil { return nil, err } return site, nil }
func (c *HomeCloud) ensureSiteExists() { conn := c.Pool.Get() defer conn.Close() site, err := c.SiteModel.Fetch(config.MustString("siteId"), conn) if err != nil && err != models.RecordNotFound { log.Fatalf("Failed to get site: %s", err) } if err == models.RecordNotFound { siteType := "home" name := "Home" site = &model.Site{ Name: &name, ID: config.MustString("siteId"), Type: &siteType, } err = c.SiteModel.Create(site, conn) if err != nil && err != models.RecordNotFound { log.Fatalf("Failed to create site: %s", err) } } }
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 (m *SiteModel) Create(site *model.Site, conn redis.Conn) error { m.syncing.Wait() //defer m.sync() if site.ID == "here" { site.ID = config.MustString("siteId") } m.log.Debugf("Saving site %s", site.ID) updated, err := m.save(site.ID, site, conn) m.log.Debugf("Site was updated? %t", updated) return err }
func req(url string, data interface{}) error { client := &http.Client{ Timeout: time.Second * 30, } if config.Bool(false, "cloud", "allowSelfSigned") { log.Warningf("Allowing self-signed cerificate (should only be used to connect to development cloud)") client.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } resp, err := client.Get(fmt.Sprintf(url, config.MustString("token"))) if err != nil { return err } body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } var response restResponse err = json.Unmarshal(body, &response) if err != nil { return err } if resp.StatusCode != http.StatusOK || response.Type == "error" { var data restError err = json.Unmarshal(response.Data, &data) if data.Type == "authentication_invalid_token" { return errorUnauthorised } if err != nil { return err } return fmt.Errorf("Error from cloud: %s (%s)", data.Message, data.Type) } return json.Unmarshal(response.Data, data) }
// update the site-preferences.json file with a copy read from the site model func listenToSiteUpdates(conn *ninja.Connection) { configSiteId := config.MustString("siteId") siteModel := conn.GetServiceClient("$home/services/SiteModel") siteModel.OnEvent("updated", func(siteId *string, values map[string]string) bool { if siteId != nil && configSiteId == *siteId { err := updateSitePreferences(siteModel, *siteId) if err != nil { log.Debugf("error ignored while updating site preferences: %v", err) } } return true }) err := updateSitePreferences(siteModel, configSiteId) if err != nil { log.Debugf("error ignored while updating site preferences: %v", err) } }
func (m *SiteModel) Update(id string, site *model.Site, conn redis.Conn) error { m.syncing.Wait() //defer m.sync() if id == "here" { id = config.MustString("siteId") } oldSite := &model.Site{} if err := m.fetch(id, oldSite, false, conn); err != nil { return fmt.Errorf("Failed to fetch site (id:%s): %s", id, err) } oldSite.Name = site.Name oldSite.Type = site.Type oldSite.SitePreferences = site.SitePreferences oldSite.DefaultRoomID = site.DefaultRoomID if site.Latitude != nil && site.Longitude != nil && ((oldSite.Latitude == nil || oldSite.Longitude == nil) || (*oldSite.Latitude != *site.Latitude || *oldSite.Longitude != *site.Longitude)) { oldSite.Latitude = site.Latitude oldSite.Longitude = site.Longitude tz, err := getTimezone(*site.Latitude, *site.Longitude) if err != nil { return fmt.Errorf("Failed to get timezone: %s", err) } else { m.log.Debugf("Timezone (%0.4f, %0.4f) -> %v", *site.Latitude, *site.Longitude, tz) } oldSite.TimeZoneID = tz.TimeZoneID oldSite.TimeZoneName = tz.TimeZoneName oldSite.TimeZoneOffset = tz.RawOffset // TODO: Not handling DST. Worth even having? } else { m.log.Debugf("no change to latitude or longitude") } if _, err := m.save(id, oldSite, conn); err != nil { return fmt.Errorf("Failed to update site (id:%s): %s", id, err) } return nil }
func getSiteLocation() { siteModel := conn.GetServiceClient("$home/services/SiteModel") for { var site model.Site err := siteModel.Call("fetch", config.MustString("siteId"), &site, time.Second*5) if err == nil && site.Latitude != nil { latitude, longitude = *site.Latitude, *site.Longitude break } log.Infof("Failed to fetch site latitude/longitude: err:%s", err) time.Sleep(time.Second * 5) } }
func (s *SchedulerService) Init(moduleID string) error { siteID := config.MustString("siteId") s.Scheduler = &controller.Scheduler{} s.Scheduler.SetLogger(s.Log) s.Scheduler.SetSiteID(siteID) s.Scheduler.SetConfigStore(s.ConfigStore) s.Scheduler.SetConnection(s.Conn, time.Millisecond*time.Duration(config.Int(10000, "scheduler", "timeout"))) var err error topic := fmt.Sprintf("$site/%s/service/%s", siteID, "scheduler") announcement := &nmodel.ServiceAnnouncement{ Schema: "http://schema.ninjablocks.com/service/scheduler", } if s.Service, err = s.Conn.ExportService(s, topic, announcement); err != nil { return err } if err := s.Scheduler.Start(s.Model); err != nil { return err } return nil }
"reflect" "strings" "github.com/davecgh/go-spew/spew" "github.com/ninjasphere/go-ninja/config" "github.com/ninjasphere/go-ninja/logger" "github.com/ninjasphere/go-ninja/model" "github.com/ninjasphere/gojsonschema" "github.com/xeipuuv/gojsonreference" ) var log = logger.GetLogger("schemas") var root = "http://schema.ninjablocks.com/" var rootURL, _ = url.Parse(root) var filePrefix = config.MustString("installDirectory") + "/sphere-schemas/" var fileSuffix = ".json" var schemaPool = gojsonschema.NewSchemaPool() var validationEnabled = config.Bool(false, "validate") func init() { schemaPool.FilePrefix = &filePrefix schemaPool.FileSuffix = &fileSuffix if validationEnabled { log.Infof("-------- VALIDATION ENABLED --------") } } func Validate(schema string, obj interface{}) (*string, error) {
volumeDevices []*ninja.ServiceClient } type MediaPaneImages struct { Volume string VolumeUp string VolumeDown string Mute string Play string Pause string Stop string Next string } var mediaImages = MediaPaneImages{ Volume: util.ResolveImagePath(config.MustString("led.media.images.volume")), VolumeUp: util.ResolveImagePath(config.MustString("led.media.images.volumeUp")), VolumeDown: util.ResolveImagePath(config.MustString("led.media.images.volumeDown")), Mute: util.ResolveImagePath(config.MustString("led.media.images.mute")), Play: util.ResolveImagePath(config.MustString("led.media.images.play")), Pause: util.ResolveImagePath(config.MustString("led.media.images.pause")), Stop: util.ResolveImagePath(config.MustString("led.media.images.stop")), Next: util.ResolveImagePath(config.MustString("led.media.images.next")), } func NewMediaPane(conn *ninja.Connection) *MediaPane { log := logger.GetLogger("MediaPane") pane := &MediaPane{ log: log, conn: conn,
func (m *TimeSeriesManager) Start() error { m.log.Infof("Starting") m.Conn.GetMqttClient().Subscribe("$device/+/channel/+/event/state", func(topic string, message []byte) { x, _ := ninja.MatchTopicPattern("$device/:device/channel/:channel/event/state", topic) values := *x thing, inCache := thingsByDeviceId[values["device"]] if !inCache { conn := m.Pool.Get() defer conn.Close() var err error thing, err = m.ThingModel.FetchByDeviceId(values["device"], conn) if err != nil { log.Errorf("Got a state event, but failed to fetch thing for device: %s error: %s", values["device"], err) return } if thing == nil { return } thingsByDeviceId[values["device"]] = thing } channel, inCache := channels[values["device"]+values["channel"]] if !inCache { conn := m.Pool.Get() defer conn.Close() var err error channel, err = m.ChannelModel.Fetch(values["device"], values["channel"], conn) if err != nil { log.Errorf("Got a state event, but failed to fetch channel: %s on device: %s error: %s", values["channel"], values["device"], err) return } channels[values["device"]+values["channel"]] = channel } var data map[string]interface{} err := json.Unmarshal(message, &data) params := data["params"] if paramsArray, ok := data["params"].([]interface{}); ok { params = paramsArray[0] } if err != nil { log.Errorf("Got a state event, but failed to unmarshal it. channel: %s on device: %s error: %s", values["channel"], values["device"], err) return } log.Debugf("Got state event from device:%s channel:%s payload:%v", values["device"], values["channel"], params) points, err := schemas.GetEventTimeSeriesData(params, channel.Schema, "state") if err != nil { log.Errorf("Got a state event, but failed to create time series points. channel: %s on device: %s error: %s", values["channel"], values["device"], err) return } if len(points) > 0 { payload := &model.TimeSeriesPayload{ Thing: thing.ID, ThingType: thing.Type, Promoted: thing.Promoted, Device: values["device"], Channel: values["channel"], Schema: channel.Schema, Event: "state", Points: points, Site: config.MustString("siteId"), Time: time.Now().Format(time.RFC3339Nano), } /*if user, ok := data["_userOverride"].(string); ok { payload.UserOverride = user } if node, ok := data["_nodeOverride"].(string); ok { payload.NodeOverride = node } if site, ok := data["_siteOverride"].(string); ok { payload.SiteOverride = site }*/ payload.TimeZone, payload.TimeOffset = time.Now().Zone() err = m.Conn.SendNotification("$ninja/services/timeseries", payload) if err != nil { log.Fatalf("Got a state event, but failed to send time series points. channel: %s on device: %s error: %s", values["channel"], values["device"], err) } } }) return nil }
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) }