// Process communication payload from device (via websocket. or REST)
//  {
//      "device_id" : "9dfe2a00-efe2-45f9-a84c-8afc69caf4e7",
//        "sddl" : {
//          "optional inbound bool onoff" : {}
//        },
//        "vars" : {
//            "temperature" : 38.0f;
//            "gps" : {
//                "latitude" : 38.0f;
//                "longitude" : 38.0f;
//            }
//        }
//    }
//  }
//
//  <conn> is an optional datalayer connection.  If provided, it is used.
//  Otherwise, a datalayer connection is opened by this routine.
//
//  <device> is the device that sent the communication.  If nil, then either
//  <deviceId> or, as a last resort, the payload's "device_id" will be used.
//
//  <deviceId> is a string device ID of the device that sent the communication.
//  This is ignored if <device> is not nil.  If nil, then the payload's
//  "device_id" will be used.
//
//  <secretKey> is the device's secret key. A secret key is required if
//  <device> is nil.  Either the value of <secretKey> or, as a last resort, the
//  payload's "secret_key" field will be used.
//
//  <payload> is a string containing the JSON payload.
func ProcessDeviceComm(
	cfg config.Config,
	conn datalayer.Connection,
	device datalayer.Device,
	deviceIdString string,
	secretKey string,
	payload string) ServiceResponse {
	var err error
	var out ServiceResponse
	var ok bool

	canolog.Info("ProcessDeviceComm STARTED")
	// If conn is nil, open a datalayer connection.
	if conn == nil {
		dl := cassandra_datalayer.NewDatalayer(cfg)
		conn, err = dl.Connect("canopy")
		if err != nil {
			return ServiceResponse{
				HttpCode: http.StatusInternalServerError,
				Err:      fmt.Errorf("Could not connect to database: %s", err),
				Response: `{"result" : "error", "error_type" : "could_not_connect_to_database"}`,
				Device:   nil,
			}
		}
		defer conn.Close()
	}

	// Parse JSON payload
	var payloadObj map[string]interface{}
	err = json.Unmarshal([]byte(payload), &payloadObj)
	if err != nil {
		return ServiceResponse{
			HttpCode: http.StatusBadRequest,
			Err:      fmt.Errorf("Error JSON decoding payload: %s", err),
			Response: `{"result" : "error", "error_type" : "decoding_paylaod"}`,
			Device:   nil,
		}
	}

	// Device can be provided to this routine in one of three ways:
	// 1) <device> parameter
	// 2) <deviceId> parameter
	// 3) "device_id" field in payload
	if device == nil && deviceIdString != "" {
		// Parse UUID
		uuid, err := gocql.ParseUUID(deviceIdString)
		if err != nil {
			return ServiceResponse{
				HttpCode: http.StatusBadRequest,
				Err:      fmt.Errorf("Invalid UUID %s: %s", deviceIdString, err),
				Response: `{"result" : "error", "error_type" : "device_uuid_required"}`,
				Device:   nil,
			}
		}

		// Get secret key from payload if necessary
		if secretKey == "" {
			secretKey, ok = payloadObj["secret_key"].(string)
			if !ok {
				return ServiceResponse{
					HttpCode: http.StatusBadRequest,
					Err:      fmt.Errorf("\"secret_key\" field must be string"),
					Response: `{"result" : "error", "error_type" : "bad_payload"}`,
					Device:   nil,
				}
			}
		}

		// lookup device
		device, err = conn.LookupDeviceVerifySecretKey(uuid, secretKey)
		if err != nil {
			return ServiceResponse{
				HttpCode: http.StatusInternalServerError,
				Err:      fmt.Errorf("Error looking up or verifying device: %s", err),
				Response: `{"result" : "error", "error_type" : "database_error"}`,
				Device:   nil,
			}
		}
	}

	// Is "device_id" provided in payload?
	_, ok = payloadObj["device_id"]
	if ok {
		deviceIdStringFromPayload, ok := payloadObj["device_id"].(string)
		if !ok {
			return ServiceResponse{
				HttpCode: http.StatusBadRequest,
				Err:      fmt.Errorf("\"device_id\" field must be string"),
				Response: `{"result" : "error", "error_type" : "bad_payload"}`,
				Device:   nil,
			}
		}

		// Parse UUID
		uuid, err := gocql.ParseUUID(deviceIdStringFromPayload)
		if err != nil {
			return ServiceResponse{
				HttpCode: http.StatusBadRequest,
				Err:      fmt.Errorf("Invalid UUID %s: %s", deviceIdStringFromPayload, err),
				Response: `{"result" : "error", "error_type" : "device_uuid_required"}`,
				Device:   nil,
			}
		}

		// Is <device> already set?
		// If not: set it.
		// If so: ensure consistency
		if device == nil {

			// Get secret key from payload if necessary
			if secretKey == "" {
				secretKey, ok = payloadObj["secret_key"].(string)
				if !ok {
					return ServiceResponse{
						HttpCode: http.StatusBadRequest,
						Err:      fmt.Errorf("\"secret_key\" field must be string"),
						Response: `{"result" : "error", "error_type" : "bad_payload"}`,
						Device:   nil,
					}
				}
			}

			// Lookup device
			device, err = conn.LookupDeviceVerifySecretKey(uuid, secretKey)
			if err != nil {
				return ServiceResponse{
					HttpCode: http.StatusInternalServerError,
					Err:      fmt.Errorf("Error looking up or verifying device: %s", err),
					Response: `{"result" : "error", "error_type" : "database_error"}`,
					Device:   nil,
				}
			}
		} else {
			if device.ID().String() != deviceIdStringFromPayload {
				return ServiceResponse{
					HttpCode: http.StatusBadRequest,
					Err:      fmt.Errorf("Inconsistent device ID: %s %s", device.ID().String(), deviceIdStringFromPayload),
					Response: `{"result" : "error", "error_type" : "bad_payload"}`,
					Device:   nil,
				}
			}
		}
	}

	// If device wasn't provided at all, throw error.
	if device == nil {
		return ServiceResponse{
			HttpCode: http.StatusBadRequest,
			Err:      fmt.Errorf("Device ID expected"),
			Response: `{"result" : "error", "error_type" : "bad_payload"}`,
			Device:   nil,
		}
	}
	out.Device = device

	device.UpdateLastActivityTime(nil)

	// If "sddl" is present, create new / reconfigure Cloud Variables.
	_, ok = payloadObj["sddl"]
	if ok {
		updateMap, ok := payloadObj["sddl"].(map[string]interface{})
		if !ok {
			return ServiceResponse{
				HttpCode: http.StatusBadRequest,
				Err:      fmt.Errorf("Expected object for \"sdd\" field"),
				Response: `{"result" : "error", "error_type" : "bad_payload"}`,
				Device:   nil,
			}
		}
		err = device.ExtendSDDL(updateMap)
		if err != nil {
			return ServiceResponse{
				HttpCode: http.StatusInternalServerError,
				Err:      fmt.Errorf("Error updating device's SDDL: %s", err),
				Response: `{"result" : "error", "error_type" : "database_error"}`,
				Device:   nil,
			}
		}
	}

	// If "vars" is present, update value of all Cloud Variables (creating new
	// Cloud Variables as necessary)
	doc := device.SDDLDocument()
	_, ok = payloadObj["vars"]
	canolog.Info("vars present:", ok)
	if ok {
		varsMap, ok := payloadObj["vars"].(map[string]interface{})
		if !ok {
			return ServiceResponse{
				HttpCode: http.StatusBadRequest,
				Err:      fmt.Errorf("Expected object for \"vars\" field"),
				Response: `{"result" : "error", "error_type" : "bad_payload"}`,
				Device:   nil,
			}
		}
		canolog.Info("varsMap: ", varsMap)
		for varName, value := range varsMap {
			varDef, err := doc.LookupVarDef(varName)
			// TODO: an error doesn't necessarily mean prop should be created?
			canolog.Info("Looking up property ", varName)
			if varDef == nil {
				// Property doesn't exist.  Add it.
				canolog.Info("Not found.  Add property ", varName)
				// TODO: What datatype?
				// TODO: What other parameters?
				varDef, err = doc.AddVarDef(varName, sddl.DATATYPE_FLOAT32)
				if err != nil {
					return ServiceResponse{
						HttpCode: http.StatusInternalServerError,
						Err:      fmt.Errorf("Error creating cloud variable %s: %s", varName, err),
						Response: `{"result" : "error", "error_type" : "database_error"}`,
						Device:   nil,
					}
				}

				// save modified SDDL
				// TODO: Save at the end?
				canolog.Info("SetSDDLDocument ", doc)
				err = device.SetSDDLDocument(doc)
				if err != nil {
					return ServiceResponse{
						HttpCode: http.StatusInternalServerError,
						Err:      fmt.Errorf("Error updating SDDL: %s", err),
						Response: `{"result" : "error", "error_type" : "database_error"}`,
						Device:   nil,
					}
				}
			}

			// Store property value.
			// Convert value datatype
			varVal, err := cloudvar.JsonToCloudVarValue(varDef, value)
			if err != nil {
				return ServiceResponse{
					HttpCode: http.StatusInternalServerError,
					Err:      fmt.Errorf("Error converting JSON to propertyValue: %s", err),
					Response: `{"result" : "error", "error_type" : "bad_payload"}`,
					Device:   nil,
				}
			}
			canolog.Info("InsertStample")
			err = device.InsertSample(varDef, time.Now(), varVal)
			if err != nil {
				return ServiceResponse{
					HttpCode: http.StatusInternalServerError,
					Err:      fmt.Errorf("Error inserting sample %s: %s", varName, err),
					Response: `{"result" : "error", "error_type" : "database_error"}`,
					Device:   nil,
				}
			}
		}
	}

	return ServiceResponse{
		HttpCode: http.StatusOK,
		Err:      nil,
		Response: `{"result" : "ok"}`,
		Device:   device,
	}
}
Exemple #2
0
func deviceToJsonObj(device datalayer.Device, timestamp_type string) (map[string]interface{}, error) {
	statusJsonObj := map[string]interface{}{
		"ws_connected": device.WSConnected(),
	}
	lastSeen := device.LastActivityTime()
	if lastSeen == nil {
		statusJsonObj["last_activity_time"] = nil
	} else {
		if timestamp_type == "epoch_us" {
			statusJsonObj["last_activity_time"] = canotime.EpochMicroseconds(*lastSeen)
		} else {
			statusJsonObj["last_activity_time"] = canotime.RFC3339(*lastSeen)
		}
	}

	out := map[string]interface{}{
		"device_id":     device.ID().String(),
		"friendly_name": device.Name(),
		"location_note": device.LocationNote(),
		"status":        statusJsonObj,
		"var_decls":     nil,
		"secret_key":    device.SecretKey(),
		"vars":          map[string]interface{}{},
		"notifs":        []interface{}{},
	}

	sddlDoc := device.SDDLDocument()
	if sddlDoc != nil {
		out["var_decls"] = sddlDoc.Json()
	}

	outDoc := device.SDDLDocument()
	if outDoc != nil {
		// get most recent value of each sensor/control
		for _, varDef := range outDoc.VarDefs() {
			sample, err := device.LatestDataByName(varDef.Name())
			if err != nil {
				continue
			}
			if timestamp_type == "epoch_us" {
				out["vars"].(map[string]interface{})[varDef.Name()] = map[string]interface{}{
					"t": canotime.EpochMicroseconds(sample.Timestamp),
					"v": sample.Value,
				}
			} else {
				out["vars"].(map[string]interface{})[varDef.Name()] = map[string]interface{}{
					"t": canotime.RFC3339(sample.Timestamp),
					"v": sample.Value,
				}
			}
		}

		// Generate JSON for notifications
		//
		/*notifications, err := device.HistoricNotifications()
		  canolog.Info("Reading notifications")
		  if err != nil {
		      canolog.Info("Error reading notifications %s", err)
		      return nil, err
		  }

		  outNotifications := []jsonNotification{};
		  for _, notification := range notifications {
		      outNotifications = append(
		              outNotifications,
		              jsonNotification{
		                  notification.Datetime().Format(time.RFC3339),
		                  notification.IsDismissed(),
		                  notification.Msg(),
		              })
		  }*/
	}

	return out, nil

}
Exemple #3
0
func NewCanopyWebsocketServer(cfg config.Config, outbox jobqueue.Outbox, pigeonServer jobqueue.Server) func(ws *websocket.Conn) {
	// Main websocket server routine.
	// This event loop runs until the websocket connection is broken.
	return func(ws *websocket.Conn) {
		canolog.Websocket("Websocket connection established")

		var cnt int32
		var device datalayer.Device
		var inbox jobqueue.Inbox
		var inboxReciever jobqueue.RecieveHandler
		lastPingTime := time.Now()

		cnt = 0

		// connect to cassandra
		dl := cassandra_datalayer.NewDatalayer(cfg)
		conn, err := dl.Connect("canopy")
		if err != nil {
			canolog.Error("Could not connect to database: ", err)
			return
		}
		defer conn.Close()

		for {
			var in string

			// check for message from client
			ws.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
			err := websocket.Message.Receive(ws, &in)
			if err == nil {
				// success, payload received
				cnt++
				resp := service.ProcessDeviceComm(cfg, conn, device, "", "", in)
				if resp.Device == nil {
					canolog.Error("Error processing device communications: ", resp.Err)
				} else {
					device = resp.Device
					if inbox == nil {
						deviceIdString := device.ID().String()
						inbox, err = pigeonServer.CreateInbox("canopy_ws:" + deviceIdString)
						if err != nil {
							canolog.Error("Error initializing inbox:", err)
							return
						}
						inboxReciever = jobqueue.NewRecieveHandler()
						inbox.SetHandler(inboxReciever)

						err = device.UpdateWSConnected(true)
						if err != nil {
							canolog.Error("Unexpected error: ", err)
						}
					}
				}
			} else if err == io.EOF {
				canolog.Websocket("Websocket connection closed")
				// connection closed
				if inbox != nil {
					if device != nil {
						err = device.UpdateWSConnected(false)
						if err != nil {
							canolog.Error("Unexpected error: ", err)
						}
					}
					inbox.Close()
				}
				return
			} else if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
				// timeout reached, no data for me this time
			} else {
				canolog.Error("Unexpected error: ", err)
			}

			// Periodically send blank message
			if time.Now().After(lastPingTime.Add(30 * time.Second)) {
				err := websocket.Message.Send(ws, "{}")
				if err != nil {
					canolog.Websocket("Websocket connection closed during ping")
					// connection closed
					if inbox != nil {
						if device != nil {
							err = device.UpdateWSConnected(false)
							if err != nil {
								canolog.Error("Unexpected error: ", err)
							}
						}
						inbox.Close()
					}
					return
				}
				canolog.Info("Pinging WS")
				lastPingTime = time.Now()
			}

			if inbox != nil {
				msg, _ := inboxReciever.Recieve(time.Duration(100 * time.Millisecond))
				if msg != nil {
					msgString, err := json.Marshal(msg)

					if err != nil {
						canolog.Error("Unexpected error: ", err)
					}

					canolog.Info("Websocket sending", msgString)
					canolog.Websocket("Websocket sending: ", msgString)
					websocket.Message.Send(ws, msgString)
				}
			}
		}
	}
}