| @@ -53,7 +53,7 @@ func main() { | |||||
| defer locTicker.Stop() | defer locTicker.Stop() | ||||
| chRaw := make(chan model.BeaconAdvertisement, 2000) | chRaw := make(chan model.BeaconAdvertisement, 2000) | ||||
| chSettings := make(chan model.SettingsVal, 5) | |||||
| chSettings := make(chan map[string]any, 5) | |||||
| wg.Add(3) | wg.Add(3) | ||||
| go kafkaclient.Consume(rawReader, chRaw, ctx, &wg) | go kafkaclient.Consume(rawReader, chRaw, ctx, &wg) | ||||
| @@ -65,7 +65,13 @@ eventLoop: | |||||
| case <-ctx.Done(): | case <-ctx.Done(): | ||||
| break eventLoop | break eventLoop | ||||
| case <-locTicker.C: | case <-locTicker.C: | ||||
| getLikelyLocations(appState, writer) | |||||
| settings := appState.GetSettings() | |||||
| switch settings.CurrentAlgorithm { | |||||
| case "filter": | |||||
| getLikelyLocations(appState, writer) | |||||
| case "ai": | |||||
| fmt.Println("AI algorithm selected") | |||||
| } | |||||
| case msg := <-chRaw: | case msg := <-chRaw: | ||||
| assignBeaconToList(msg, appState) | assignBeaconToList(msg, appState) | ||||
| case msg := <-chSettings: | case msg := <-chSettings: | ||||
| @@ -132,32 +138,6 @@ func getLikelyLocations(appState *appcontext.AppState, writer *kafka.Writer) { | |||||
| if beacon.LocationConfidence == settings.LocationConfidence && beacon.PreviousConfidentLocation != bestLocName { | if beacon.LocationConfidence == settings.LocationConfidence && beacon.PreviousConfidentLocation != bestLocName { | ||||
| beacon.LocationConfidence = 0 | beacon.LocationConfidence = 0 | ||||
| // js, err := json.Marshal(model.LocationChange{ | |||||
| // Method: "LocationChange", | |||||
| // BeaconRef: beacon, | |||||
| // Name: beacon.Name, | |||||
| // PreviousLocation: beacon.PreviousConfidentLocation, | |||||
| // NewLocation: bestLocName, | |||||
| // Timestamp: time.Now().Unix(), | |||||
| // }) | |||||
| // if err != nil { | |||||
| // eMsg := fmt.Sprintf("Error in marshaling: %v", err) | |||||
| // slog.Error(eMsg) | |||||
| // beacon.PreviousConfidentLocation = bestLocName | |||||
| // beacon.PreviousLocation = bestLocName | |||||
| // appState.UpdateBeacon(beacon.ID, beacon) | |||||
| // continue | |||||
| // } | |||||
| // msg := kafka.Message{ | |||||
| // Value: js, | |||||
| // } | |||||
| // err = writer.WriteMessages(context.Background(), msg) | |||||
| // if err != nil { | |||||
| // fmt.Println("Error in sending Kafka message") | |||||
| // } | |||||
| } | } | ||||
| beacon.PreviousLocation = bestLocName | beacon.PreviousLocation = bestLocName | ||||
| @@ -114,9 +114,6 @@ func main() { | |||||
| r := mux.NewRouter() | r := mux.NewRouter() | ||||
| r.HandleFunc("/api/settings", controller.SettingsListController(appState, ctx)).Methods("GET") | |||||
| r.HandleFunc("/api/settings", controller.SettingsEditController(settingsWriter, appState, ctx)).Methods("POST") | |||||
| r.HandleFunc("/reslevis/getGateways", controller.GatewayListController(db)).Methods("GET") | r.HandleFunc("/reslevis/getGateways", controller.GatewayListController(db)).Methods("GET") | ||||
| r.HandleFunc("/reslevis/postGateway", controller.GatewayAddController(db)).Methods("POST") | r.HandleFunc("/reslevis/postGateway", controller.GatewayAddController(db)).Methods("POST") | ||||
| r.HandleFunc("/reslevis/removeGateway/{id}", controller.GatewayDeleteController(db)).Methods("DELETE") | r.HandleFunc("/reslevis/removeGateway/{id}", controller.GatewayDeleteController(db)).Methods("DELETE") | ||||
| @@ -142,6 +139,9 @@ func main() { | |||||
| r.HandleFunc("/configs/beacons/{id}", controller.ParserUpdateController(db, parserWriter, ctx)).Methods("PUT") | r.HandleFunc("/configs/beacons/{id}", controller.ParserUpdateController(db, parserWriter, ctx)).Methods("PUT") | ||||
| r.HandleFunc("/configs/beacons/{id}", controller.ParserDeleteController(db, parserWriter, ctx)).Methods("DELETE") | r.HandleFunc("/configs/beacons/{id}", controller.ParserDeleteController(db, parserWriter, ctx)).Methods("DELETE") | ||||
| r.HandleFunc("/reslevis/settings", controller.SettingsUpdateController(db, settingsWriter, ctx)).Methods("PATCH") | |||||
| r.HandleFunc("/reslevis/settings", controller.SettingsListController(db)).Methods("GET") | |||||
| wsHandler := http.HandlerFunc(serveWs(db, ctx)) | wsHandler := http.HandlerFunc(serveWs(db, ctx)) | ||||
| restApiHandler := handlers.CORS(originsOk, headersOk, methodsOk)(r) | restApiHandler := handlers.CORS(originsOk, headersOk, methodsOk)(r) | ||||
| mainHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | mainHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| @@ -28,6 +28,7 @@ require ( | |||||
| github.com/jinzhu/now v1.1.5 // indirect | github.com/jinzhu/now v1.1.5 // indirect | ||||
| github.com/joho/godotenv v1.5.1 // indirect | github.com/joho/godotenv v1.5.1 // indirect | ||||
| github.com/klauspost/compress v1.15.9 // indirect | github.com/klauspost/compress v1.15.9 // indirect | ||||
| github.com/mitchellh/mapstructure v1.5.0 // indirect | |||||
| github.com/pierrec/lz4/v4 v4.1.15 // indirect | github.com/pierrec/lz4/v4 v4.1.15 // indirect | ||||
| golang.org/x/crypto v0.42.0 // indirect | golang.org/x/crypto v0.42.0 // indirect | ||||
| golang.org/x/net v0.44.0 // indirect | golang.org/x/net v0.44.0 // indirect | ||||
| @@ -39,6 +39,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | |||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||
| github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= | github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= | ||||
| github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= | github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= | ||||
| github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | |||||
| github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | |||||
| github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= | github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= | ||||
| github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= | github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| @@ -6,6 +6,7 @@ import ( | |||||
| "time" | "time" | ||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/mitchellh/mapstructure" | |||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| ) | ) | ||||
| @@ -31,15 +32,14 @@ func NewAppState() *AppState { | |||||
| Results: make(map[string]model.HTTPResult), | Results: make(map[string]model.HTTPResult), | ||||
| }, | }, | ||||
| settings: model.Settings{ | settings: model.Settings{ | ||||
| Settings: model.SettingsVal{ | |||||
| LocationConfidence: 4, | |||||
| LastSeenThreshold: 15, | |||||
| BeaconMetricSize: 30, | |||||
| HASendInterval: 5, | |||||
| HASendChangesOnly: false, | |||||
| RSSIEnforceThreshold: false, | |||||
| RSSIMinThreshold: 100, | |||||
| }, | |||||
| CurrentAlgorithm: "filter", // possible values filter or AI | |||||
| LocationConfidence: 4, | |||||
| LastSeenThreshold: 15, | |||||
| BeaconMetricSize: 30, | |||||
| HASendInterval: 5, | |||||
| HASendChangesOnly: false, | |||||
| RSSIEnforceThreshold: false, | |||||
| RSSIMinThreshold: 100, | |||||
| }, | }, | ||||
| beaconEvents: model.BeaconEventList{ | beaconEvents: model.BeaconEventList{ | ||||
| Beacons: make(map[string]model.BeaconEvent), | Beacons: make(map[string]model.BeaconEvent), | ||||
| @@ -283,17 +283,13 @@ func (m *AppState) GetBeaconCount() int { | |||||
| } | } | ||||
| // GetSettingsValue returns current settings as a value | // GetSettingsValue returns current settings as a value | ||||
| func (m *AppState) GetSettingsValue() model.SettingsVal { | |||||
| m.settings.Lock.RLock() | |||||
| defer m.settings.Lock.RUnlock() | |||||
| return m.settings.Settings | |||||
| func (m *AppState) GetSettingsValue() model.Settings { | |||||
| return m.settings | |||||
| } | } | ||||
| // UpdateSettings updates the system settings (thread-safe) | // UpdateSettings updates the system settings (thread-safe) | ||||
| func (m *AppState) UpdateSettings(newSettings model.SettingsVal) { | |||||
| m.settings.Lock.Lock() | |||||
| defer m.settings.Lock.Unlock() | |||||
| m.settings.Settings = newSettings | |||||
| func (m *AppState) UpdateSettings(settings map[string]any) { | |||||
| if err := mapstructure.Decode(settings, &m.settings); err != nil { | |||||
| fmt.Printf("Error in persisting settings: %v\n", err) | |||||
| } | |||||
| } | } | ||||
| @@ -3,64 +3,51 @@ package controller | |||||
| import ( | import ( | ||||
| "context" | "context" | ||||
| "encoding/json" | "encoding/json" | ||||
| "fmt" | |||||
| "net/http" | "net/http" | ||||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| "gorm.io/gorm" | |||||
| ) | ) | ||||
| func SettingsEditController(writer *kafka.Writer, appstate *appcontext.AppState, ctx context.Context) http.HandlerFunc { | |||||
| func SettingsListController(db *gorm.DB) http.HandlerFunc { | |||||
| return func(w http.ResponseWriter, r *http.Request) { | return func(w http.ResponseWriter, r *http.Request) { | ||||
| decoder := json.NewDecoder(r.Body) | |||||
| var inSettings model.SettingsVal | |||||
| if err := decoder.Decode(&inSettings); err != nil { | |||||
| var settings []model.Settings | |||||
| db.Find(&settings) | |||||
| res, err := json.Marshal(settings) | |||||
| if err != nil { | |||||
| http.Error(w, err.Error(), 400) | http.Error(w, err.Error(), 400) | ||||
| fmt.Println("Error in decoding Settings body: ", err) | |||||
| return | return | ||||
| } | } | ||||
| if !settingsCheck(inSettings) { | |||||
| http.Error(w, "values must be greater than 0", 400) | |||||
| fmt.Println("settings values must be greater than 0") | |||||
| return | |||||
| } | |||||
| w.Write(res) | |||||
| } | |||||
| } | |||||
| valueStr, err := json.Marshal(&inSettings) | |||||
| if err != nil { | |||||
| http.Error(w, "Error in encoding settings", 500) | |||||
| fmt.Println("Error in encoding settings: ", err) | |||||
| func SettingsUpdateController(db *gorm.DB, writer *kafka.Writer, ctx context.Context) http.HandlerFunc { | |||||
| return func(w http.ResponseWriter, r *http.Request) { | |||||
| var updates map[string]any | |||||
| if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { | |||||
| http.Error(w, "Invalid JSON", 400) | |||||
| return | return | ||||
| } | } | ||||
| msg := kafka.Message{ | |||||
| Value: valueStr, | |||||
| if err := db.Model(&model.Settings{}).Where("id = ?", 1).Updates(updates).Error; err != nil { | |||||
| http.Error(w, err.Error(), 500) | |||||
| return | |||||
| } | } | ||||
| if err := writer.WriteMessages(ctx, msg); err != nil { | |||||
| fmt.Println("error in sending Kafka message") | |||||
| http.Error(w, "Error in sending kafka message", 500) | |||||
| eMsg, err := json.Marshal(updates) | |||||
| if err != nil { | |||||
| http.Error(w, "Error in marshaling settings updates", 400) | |||||
| return | return | ||||
| } | } | ||||
| msg := kafka.Message{ | |||||
| Value: eMsg, | |||||
| } | |||||
| // if all is OK persist settings | |||||
| appstate.UpdateSettings(inSettings) | |||||
| w.Write([]byte("ok")) | |||||
| } | |||||
| } | |||||
| func settingsCheck(settings model.SettingsVal) bool { | |||||
| if settings.LocationConfidence <= 0 || settings.LastSeenThreshold <= 0 || settings.HASendInterval <= 0 { | |||||
| return false | |||||
| } | |||||
| return true | |||||
| } | |||||
| func SettingsListController(appstate *appcontext.AppState, ctx context.Context) http.HandlerFunc { | |||||
| return func(w http.ResponseWriter, r *http.Request) { | |||||
| writer.WriteMessages(ctx, msg) | |||||
| w.Write([]byte("Settings updated")) | |||||
| } | } | ||||
| } | } | ||||
| @@ -26,7 +26,7 @@ func Connect(cfg *config.Config) (*gorm.DB, error) { | |||||
| return nil, err | return nil, err | ||||
| } | } | ||||
| if err := db.AutoMigrate(&model.Gateway{}, model.Zone{}, model.TrackerZones{}, model.Tracker{}, model.Config{}); err != nil { | |||||
| if err := db.AutoMigrate(&model.Gateway{}, model.Zone{}, model.TrackerZones{}, model.Tracker{}, model.Config{}, model.Settings{}); err != nil { | |||||
| return nil, err | return nil, err | ||||
| } | } | ||||
| @@ -0,0 +1,12 @@ | |||||
| package model | |||||
| type Settings struct { | |||||
| CurrentAlgorithm string | |||||
| LocationConfidence int64 | |||||
| LastSeenThreshold int64 | |||||
| BeaconMetricSize int | |||||
| HASendInterval int | |||||
| HASendChangesOnly bool | |||||
| RSSIEnforceThreshold bool | |||||
| RSSIMinThreshold int64 | |||||
| } | |||||
| @@ -6,22 +6,6 @@ import ( | |||||
| "github.com/segmentio/kafka-go" | "github.com/segmentio/kafka-go" | ||||
| ) | ) | ||||
| // Settings defines configuration parameters for presence detection behavior. | |||||
| type SettingsVal struct { | |||||
| LocationConfidence int64 `json:"location_confidence"` | |||||
| LastSeenThreshold int64 `json:"last_seen_threshold"` | |||||
| BeaconMetricSize int `json:"beacon_metrics_size"` | |||||
| HASendInterval int64 `json:"ha_send_interval"` | |||||
| HASendChangesOnly bool `json:"ha_send_changes_only"` | |||||
| RSSIMinThreshold int64 `json:"rssi_min_threshold"` | |||||
| RSSIEnforceThreshold bool `json:"enforce_rssi_threshold"` | |||||
| } | |||||
| type Settings struct { | |||||
| Settings SettingsVal | |||||
| Lock sync.RWMutex | |||||
| } | |||||
| // BeaconAdvertisement represents the JSON payload received from beacon advertisements. | // BeaconAdvertisement represents the JSON payload received from beacon advertisements. | ||||
| type BeaconAdvertisement struct { | type BeaconAdvertisement struct { | ||||
| ID string | ID string | ||||
| @@ -60,23 +60,6 @@ func LocationToBeaconService(msg model.HTTPLocation, db *gorm.DB, writer *kafka. | |||||
| } | } | ||||
| } | } | ||||
| // func LocationToBeaconService(msg model.HTTPLocation, appState *appcontext.AppState, ctx context.Context) error { | |||||
| // id := msg.ID | |||||
| // beacon, ok := appState.GetHTTPResult(id) | |||||
| // if !ok { | |||||
| // appState.UpdateHTTPResult(id, model.HTTPResult{ID: id, Position: msg.Location, Distance: msg.Distance, LastSeen: msg.LastSeen, PreviousConfidentLocation: msg.PreviousConfidentLocation}) | |||||
| // } else { | |||||
| // beacon.ID = id | |||||
| // beacon.Position = msg.Location | |||||
| // beacon.Distance = msg.Distance | |||||
| // beacon.LastSeen = msg.LastSeen | |||||
| // beacon.PreviousConfidentLocation = msg.PreviousConfidentLocation | |||||
| // appState.UpdateHTTPResult(id, beacon) | |||||
| // } | |||||
| // return nil | |||||
| // } | |||||
| func EventToBeaconService(msg model.BeaconEvent, appState *appcontext.AppState, ctx context.Context) error { | func EventToBeaconService(msg model.BeaconEvent, appState *appcontext.AppState, ctx context.Context) error { | ||||
| id := msg.ID | id := msg.ID | ||||
| beacon, ok := appState.GetHTTPResult(id) | beacon, ok := appState.GetHTTPResult(id) | ||||