| @@ -3,7 +3,6 @@ package server | |||
| import ( | |||
| "context" | |||
| "encoding/json" | |||
| "fmt" | |||
| "log/slog" | |||
| "time" | |||
| @@ -57,7 +56,6 @@ func RunEventLoop(ctx context.Context, a *ServerApp) { | |||
| continue | |||
| } | |||
| if msg.Type == "Eddystone" && msg.Battery < 3000 { | |||
| fmt.Printf("Sending alert for battery low: %+v\n", msg) | |||
| service.SendAlert(id, "BatteryLow", a.KafkaManager.GetWriter("alert"), ctx, a.DB) | |||
| } | |||
| case <-beaconTicker.C: | |||
| @@ -57,8 +57,14 @@ func (a *ServerApp) RegisterRoutes() http.Handler { | |||
| r.HandleFunc("/reslevis/alerts/{id}", controller.AlertUpdateStatusController(a.DB, a.ctx)).Methods("PATCH") | |||
| r.HandleFunc("/reslevis/alerts/{id}", controller.AlertDeleteController(a.DB, a.ctx)).Methods("DELETE") | |||
| r.HandleFunc("/reslevis/getFloors", controller.FloorListController(a.DB, a.ctx)).Methods("GET") | |||
| r.HandleFunc("/reslevis/postFloor", controller.FloorAddController(a.DB, a.ctx)).Methods("POST") | |||
| r.HandleFunc("/reslevis/updateFloor", controller.FloorUpdateController(a.DB, a.ctx)).Methods("PUT") | |||
| r.HandleFunc("/reslevis/removeFloor/{id}", controller.FloorDeleteController(a.DB, a.ctx)).Methods("DELETE") | |||
| // Tracks | |||
| r.HandleFunc("/reslevis/getTracks/{id}", controller.TracksListController(a.DB, a.ctx)).Methods("GET") | |||
| r.HandleFunc("/reslevis/getTracks", controller.TracksListAllController(a.DB, a.ctx)).Methods("GET") | |||
| r.HandleFunc("/reslevis/health", controller.HealthController(a.AppState, a.ctx)).Methods("GET") | |||
| @@ -26,7 +26,6 @@ func GetToken(ctx context.Context, cfg *config.Config, client *http.Client) (str | |||
| req, err := http.NewRequest("POST", fmt.Sprintf("%s/realms/API.Server.local/protocol/openid-connect/token", cfg.APIAuthURL), strings.NewReader(formData.Encode())) | |||
| if err != nil { | |||
| fmt.Println("error", err) | |||
| return "", err | |||
| } | |||
| req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | |||
| @@ -34,16 +33,16 @@ func GetToken(ctx context.Context, cfg *config.Config, client *http.Client) (str | |||
| req = req.WithContext(ctx) | |||
| res, err := client.Do(req) | |||
| if err != nil { | |||
| fmt.Println("error", err) | |||
| return "", err | |||
| } | |||
| var j response | |||
| if err := json.NewDecoder(res.Body).Decode(&j); err != nil { | |||
| fmt.Println("error", err) | |||
| return "", err | |||
| } | |||
| fmt.Printf("token: %s\n", j.Token) | |||
| return j.Token, nil | |||
| } | |||
| @@ -12,14 +12,12 @@ import ( | |||
| func GetTrackers(token string, client *http.Client, cfg *config.Config) ([]model.Tracker, error) { | |||
| res, err := getRequest(token, "getTrackers", client, cfg) | |||
| if err != nil { | |||
| fmt.Printf("error get trackers: %+v\n", err) | |||
| return []model.Tracker{}, err | |||
| } | |||
| var i []model.Tracker | |||
| err = json.NewDecoder(res.Body).Decode(&i) | |||
| if err != nil { | |||
| fmt.Printf("error decode trackers: %+v\n", err) | |||
| return []model.Tracker{}, err | |||
| } | |||
| @@ -29,7 +27,6 @@ func GetTrackers(token string, client *http.Client, cfg *config.Config) ([]model | |||
| func GetGateways(token string, client *http.Client, cfg *config.Config) ([]model.Gateway, error) { | |||
| res, err := getRequest(token, "getGateways", client, cfg) | |||
| if err != nil { | |||
| fmt.Printf("error get gateways: %+v\n", err) | |||
| return []model.Gateway{}, err | |||
| } | |||
| @@ -76,7 +73,6 @@ func InferPosition(token string, client *http.Client, cfg *config.Config) (model | |||
| url := fmt.Sprintf("%s/ble-ai/infer", cfg.APIBaseURL) | |||
| req, err := http.NewRequest("GET", url, nil) | |||
| if err != nil { | |||
| fmt.Printf("error new request: %+v\n", err) | |||
| return model.PositionResponse{}, err | |||
| } | |||
| @@ -84,20 +80,16 @@ func InferPosition(token string, client *http.Client, cfg *config.Config) (model | |||
| res, err := client.Do(req) | |||
| if err != nil { | |||
| fmt.Printf("error do request: %+v\n", err) | |||
| return model.PositionResponse{}, err | |||
| } | |||
| fmt.Printf("res.status: %s\n", res.Status) | |||
| if res.StatusCode != 200 { | |||
| fmt.Printf("error status code: %d\n", res.StatusCode) | |||
| return model.PositionResponse{}, fmt.Errorf("status code: %d", res.StatusCode) | |||
| } | |||
| var i model.PositionResponse | |||
| err = json.NewDecoder(res.Body).Decode(&i) | |||
| if err != nil { | |||
| fmt.Printf("error decode response: %+v\n", err) | |||
| return model.PositionResponse{}, err | |||
| } | |||
| @@ -31,8 +31,7 @@ func UpdateDB(db *gorm.DB, ctx context.Context, cfg *config.Config, writer *kafk | |||
| if trackers, err := GetTrackers(token, client, cfg); err == nil { | |||
| syncTable(db, trackers) | |||
| if err := controller.SendKafkaMessage(writer, &model.ApiUpdate{Method: "DELETE", MAC: "all"}, ctx); err != nil { | |||
| msg := fmt.Sprintf("Error in sending delete all from lookup message: %v", err) | |||
| slog.Error(msg) | |||
| slog.Error("Error in sending delete all from lookup message", "error", err) | |||
| } | |||
| for _, v := range trackers { | |||
| @@ -64,7 +63,6 @@ func UpdateDB(db *gorm.DB, ctx context.Context, cfg *config.Config, writer *kafk | |||
| if inferredPosition, err := InferPosition(token, client, cfg); err == nil { | |||
| for _, v := range inferredPosition.Items { | |||
| mac := convertMac(v.Mac) | |||
| fmt.Println(mac) | |||
| db.Model(&model.Tracker{}).Where("mac = ?", mac).Update("x", v.X).Update("y", v.Y) | |||
| } | |||
| } | |||
| @@ -14,7 +14,6 @@ func setHeader(req *http.Request, token string) { | |||
| func getRequest(token, route string, client *http.Client, cfg *config.Config) (*http.Response, error) { | |||
| url := fmt.Sprintf("%s/reslevis/%s", cfg.APIBaseURL, route) | |||
| fmt.Printf("url: %s\n", url) | |||
| req, err := http.NewRequest("GET", url, nil) | |||
| if err != nil { | |||
| return nil, err | |||
| @@ -3,7 +3,6 @@ package bridge | |||
| import ( | |||
| "context" | |||
| "encoding/json" | |||
| "fmt" | |||
| "log/slog" | |||
| "strings" | |||
| "time" | |||
| @@ -60,7 +59,7 @@ func HandleMQTTMessage(topic string, payload []byte, appState *appcontext.AppSta | |||
| } | |||
| return | |||
| } else { | |||
| fmt.Println("CSV message: ", msgStr) | |||
| slog.Debug("CSV message received", "topic", topic, "message", msgStr) | |||
| } | |||
| // CSV format: validate minimum fields (e.g. 6 columns); full parsing can be added later | |||
| s := strings.Split(msgStr, ",") | |||
| @@ -1,8 +1,6 @@ | |||
| package utils | |||
| import ( | |||
| "fmt" | |||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| ) | |||
| @@ -45,14 +43,11 @@ func LoopADStructures(b []byte, i [][2]int, id string, parserRegistry *model.Par | |||
| for _, r := range i { | |||
| ad := b[r[0]:r[1]] | |||
| if !isValidADStructure(ad) { | |||
| fmt.Println("invalid ad structure: ", beacon) | |||
| break | |||
| } | |||
| for name, parser := range parserRegistry.ParserList { | |||
| if parser.CanParse(ad) { | |||
| event, ok := parser.Parse(name, ad) | |||
| fmt.Println("beacon id: ", id) | |||
| fmt.Println("parser can parse: ", name) | |||
| if ok { | |||
| event.ID = id | |||
| event.Name = id | |||
| @@ -60,7 +55,6 @@ func LoopADStructures(b []byte, i [][2]int, id string, parserRegistry *model.Par | |||
| } | |||
| } | |||
| } | |||
| // fmt.Println("no parser can parse: ", beacon) | |||
| } | |||
| return be | |||
| @@ -0,0 +1,75 @@ | |||
| package controller | |||
| import ( | |||
| "context" | |||
| "encoding/json" | |||
| "net/http" | |||
| "github.com/AFASystems/presence/internal/pkg/api/response" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/AFASystems/presence/internal/pkg/validation" | |||
| "github.com/gorilla/mux" | |||
| "gorm.io/gorm" | |||
| ) | |||
| func FloorAddController(db *gorm.DB, context context.Context) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| var floor model.Floor | |||
| if err := json.NewDecoder(r.Body).Decode(&floor); err != nil { | |||
| response.BadRequest(w, "invalid request body") | |||
| return | |||
| } | |||
| if err := validation.Struct(&floor); err != nil { | |||
| response.BadRequest(w, err.Error()) | |||
| return | |||
| } | |||
| if err := db.WithContext(context).Create(&floor).Error; err != nil { | |||
| response.InternalError(w, "failed to create floor", err) | |||
| return | |||
| } | |||
| response.JSON(w, http.StatusCreated, map[string]string{"status": "created"}) | |||
| } | |||
| } | |||
| func FloorListController(db *gorm.DB, context context.Context) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| var floors []model.Floor | |||
| if err := db.WithContext(context).Find(&floors).Error; err != nil { | |||
| response.InternalError(w, "failed to list floors", err) | |||
| return | |||
| } | |||
| if err := db.WithContext(context).Find(&floors).Error; err != nil { | |||
| response.InternalError(w, "failed to list floors", err) | |||
| return | |||
| } | |||
| response.JSON(w, http.StatusOK, floors) | |||
| } | |||
| } | |||
| func FloorUpdateController(db *gorm.DB, context context.Context) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| var floor model.Floor | |||
| if err := json.NewDecoder(r.Body).Decode(&floor); err != nil { | |||
| response.BadRequest(w, "invalid request body") | |||
| return | |||
| } | |||
| } | |||
| } | |||
| func FloorDeleteController(db *gorm.DB, context context.Context) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| id := mux.Vars(r)["id"] | |||
| res := db.WithContext(context).Delete(&model.Floor{}, "id = ?", id) | |||
| if res.RowsAffected == 0 { | |||
| response.NotFound(w, "floor not found") | |||
| return | |||
| } | |||
| if res.Error != nil { | |||
| response.InternalError(w, "failed to delete floor", res.Error) | |||
| return | |||
| } | |||
| response.JSON(w, http.StatusOK, map[string]string{"status": "deleted"}) | |||
| } | |||
| } | |||
| @@ -2,6 +2,7 @@ package controller | |||
| import ( | |||
| "context" | |||
| "log/slog" | |||
| "net/http" | |||
| "strconv" | |||
| "time" | |||
| @@ -42,3 +43,53 @@ func TracksListController(db *gorm.DB, context context.Context) http.HandlerFunc | |||
| response.JSON(w, http.StatusOK, tracks) | |||
| } | |||
| } | |||
| func TracksListAllController(db *gorm.DB, context context.Context) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| query := r.URL.Query() | |||
| lStr := query.Get("limit") | |||
| if lStr == "" { | |||
| lStr = "100" | |||
| } | |||
| limit, err := strconv.Atoi(lStr) | |||
| if err != nil { | |||
| slog.Error("Error in converting limit string to integer value") | |||
| response.InternalError(w, "internal server error", err) | |||
| return | |||
| } | |||
| offsetStr := query.Get("offset") | |||
| if offsetStr == "" { | |||
| offsetStr = "0" | |||
| } | |||
| offset, err := strconv.Atoi(offsetStr) | |||
| if err != nil { | |||
| slog.Error("Error in converting offset string to integer value") | |||
| response.InternalError(w, "internal server error", err) | |||
| return | |||
| } | |||
| var tracks []model.Tracks | |||
| // Subquery to get the latest timestamp per tracker | |||
| subQuery := db.Model(&model.Tracks{}). | |||
| Select("tracker, MAX(timestamp) AS max_timestamp"). | |||
| Group("tracker") | |||
| if err := db.WithContext(context). | |||
| Table("tracks"). | |||
| Select("tracks.*"). | |||
| Joins("JOIN (?) AS latest ON tracks.tracker = latest.tracker AND tracks.timestamp = latest.max_timestamp", subQuery). | |||
| Order("tracks.timestamp DESC"). | |||
| Limit(limit). | |||
| Offset(offset). | |||
| Find(&tracks).Error; err != nil { | |||
| response.InternalError(w, "failed to list tracks", err) | |||
| return | |||
| } | |||
| response.JSON(w, http.StatusOK, tracks) | |||
| } | |||
| } | |||
| @@ -26,7 +26,7 @@ func Connect(cfg *config.Config) (*gorm.DB, error) { | |||
| return nil, err | |||
| } | |||
| if err := db.AutoMigrate(&model.Gateway{}, model.Zone{}, model.TrackerZones{}, model.Tracker{}, model.Config{}, appcontext.Settings{}, model.Tracks{}, &model.Alert{}); err != nil { | |||
| if err := db.AutoMigrate(&model.Gateway{}, model.Zone{}, model.TrackerZones{}, model.Tracker{}, model.Config{}, appcontext.Settings{}, model.Tracks{}, &model.Alert{}, model.Floor{}); err != nil { | |||
| return nil, err | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| package model | |||
| type Floor struct { | |||
| ID string `gorm:"unique;primaryKey"` | |||
| Name string `json:"name"` | |||
| FloorNumber int `json:"floornumber"` | |||
| Image string `json:"image"` | |||
| Description string `json:"description"` | |||
| Scale int `json:"scale"` | |||
| Building string `json:"building"` | |||
| } | |||
| @@ -17,4 +17,7 @@ type Tracks struct { | |||
| Floor string `json:"floor"` | |||
| Signal int64 `json:"signal"` | |||
| Building string `json:"building"` | |||
| X float32 `json:"x"` | |||
| Y float32 `json:"y"` | |||
| Z float32 `json:"z"` | |||
| } | |||
| @@ -75,7 +75,29 @@ func LocationToBeaconService(msg model.HTTPLocation, db *gorm.DB, writer *kafka. | |||
| return | |||
| } | |||
| if err := db.Create(&model.Tracks{UUID: msg.ID, Timestamp: time.Now(), Gateway: gw.ID, GatewayMac: gw.MAC, Tracker: msg.ID, Floor: gw.Floor, Building: gw.Building, TrackerMac: tracker.MAC, Signal: msg.RSSI}).Error; err != nil { | |||
| var floor model.Floor | |||
| if err := db.Where("id = ?", gw.Floor).First(&floor).Error; err != nil { | |||
| msg := fmt.Sprintf("Floor not found for ID: %s", gw.Floor) | |||
| slog.Error(msg) | |||
| return | |||
| } | |||
| fmt.Printf("floor: %d\n", floor.FloorNumber) | |||
| if err := db.Create(&model.Tracks{ | |||
| UUID: msg.ID, | |||
| Timestamp: time.Now(), | |||
| Gateway: gw.ID, | |||
| GatewayMac: gw.MAC, | |||
| Tracker: msg.ID, | |||
| Floor: gw.Floor, | |||
| Building: gw.Building, | |||
| TrackerMac: tracker.MAC, | |||
| Signal: msg.RSSI, | |||
| X: gw.X, | |||
| Y: gw.Y, | |||
| Z: float32(floor.FloorNumber), | |||
| }).Error; err != nil { | |||
| msg := fmt.Sprintf("Error in saving distance for beacon: %v", err) | |||
| slog.Error(msg) | |||
| return | |||
| @@ -92,6 +114,7 @@ func LocationToBeaconService(msg model.HTTPLocation, db *gorm.DB, writer *kafka. | |||
| } | |||
| func LocationToBeaconServiceAI(msg model.HTTPLocation, db *gorm.DB, writer *kafka.Writer, ctx context.Context) { | |||
| fmt.Printf("msg: %+v\n", msg) | |||
| tracker, err := findTracker(msg, db) | |||
| if err != nil { | |||
| msg := fmt.Sprintf("Error in finding tracker: %v", err) | |||
| @@ -113,7 +136,9 @@ func LocationToBeaconServiceAI(msg model.HTTPLocation, db *gorm.DB, writer *kafk | |||
| return | |||
| } | |||
| if err := db.Create(&model.Tracks{UUID: tracker.ID, Timestamp: time.Now(), Gateway: gw.ID, GatewayMac: gw.MAC, Tracker: tracker.ID, Floor: gw.Floor, Building: gw.Building, TrackerMac: tracker.MAC}).Error; err != nil { | |||
| // fmt.Printf("gw: %+v\n", gw) | |||
| if err := db.Create(&model.Tracks{UUID: tracker.ID, Timestamp: time.Now(), Gateway: gw.ID, GatewayMac: gw.MAC, Tracker: tracker.ID, Floor: gw.Floor, Building: gw.Building, TrackerMac: tracker.MAC, X: msg.X, Y: msg.Y, Z: msg.Z}).Error; err != nil { | |||
| msg := fmt.Sprintf("Error in saving distance for beacon: %v", err) | |||
| slog.Error(msg) | |||
| return | |||
| @@ -31,3 +31,12 @@ echo "===================================" | |||
| echo "Query Parameters: limit, from (RFC3339), to (RFC3339)" | |||
| echo "Get tracker UUIDs from: GET /reslevis/getTrackers" | |||
| echo "===================================" | |||
| echo "4. Get all latest positions for all trackers:" | |||
| curl -s -X GET "${BASE_URL}/reslevis/getTracks" | jq '.' | |||
| echo -e "\n" | |||
| echo "===================================" | |||
| echo "Query Parameters: limit, from (RFC3339), to (RFC3339)" | |||
| echo "Get tracker UUIDs from: GET /reslevis/getTrackers" | |||
| echo "===================================" | |||
| @@ -1,20 +1,20 @@ | |||
| #!/bin/bash | |||
| # Build the server | |||
| docker build -t afasystemadmin/ble-ai-localizer:server_v1.1 -f ../../build/package/Dockerfile.server ../../ | |||
| ¸ | |||
| docker build -t afasystemadmin/ble-ai-localizer:server_v1.2 -f ../../build/package/Dockerfile.server ../../ | |||
| # Build the location | |||
| docker build -t afasystemadmin/ble-ai-localizer:location_v1.1 -f ../../build/package/Dockerfile.location ../../ | |||
| docker build -t afasystemadmin/ble-ai-localizer:location_v1.2 -f ../../build/package/Dockerfile.location ../../ | |||
| # Build the decoder | |||
| docker build -t afasystemadmin/ble-ai-localizer:decoder_v1.1 -f ../../build/package/Dockerfile.decoder ../../ | |||
| docker build -t afasystemadmin/ble-ai-localizer:decoder_v1.2 -f ../../build/package/Dockerfile.decoder ../../ | |||
| # Build the bridge | |||
| docker build -t afasystemadmin/ble-ai-localizer:bridge_v1.1 -f ../../build/package/Dockerfile.bridge ../../ | |||
| docker build -t afasystemadmin/ble-ai-localizer:bridge_v1.2 -f ../../build/package/Dockerfile.bridge ../../ | |||
| docker image ls | |||
| docker push afasystemadmin/ble-ai-localizer:server_v1.1 | |||
| docker push afasystemadmin/ble-ai-localizer:location_v1.1 | |||
| docker push afasystemadmin/ble-ai-localizer:decoder_v1.1 | |||
| docker push afasystemadmin/ble-ai-localizer:bridge_v1.1 | |||
| docker push afasystemadmin/ble-ai-localizer:server_v1.2 | |||
| docker push afasystemadmin/ble-ai-localizer:location_v1.2 | |||
| docker push afasystemadmin/ble-ai-localizer:decoder_v1.2 | |||
| docker push afasystemadmin/ble-ai-localizer:bridge_v1.2 | |||