diff --git a/gateway-classification/docker-compose.yaml b/gateway-classification/docker-compose.yaml new file mode 100644 index 0000000..c23ef5d --- /dev/null +++ b/gateway-classification/docker-compose.yaml @@ -0,0 +1,44 @@ +## Docker Testing + +version: "3.7" +services: + mqtt-broker: + image: eclipse-mosquitto + container_name: mqtt-broker + ports: + - "1883:1883" #default mqtt port + - "9001:9001" #default mqtt port for websockets + volumes: + - ./mqtt/config:/mosquitto/config + - ./mqtt/data:/mosquitto/data + - ./mqtt/log:/mosquitto/log/mosquitto.log + + mqtt-client: + image: emqx/mqttx-web + container_name: mqtt-client + ports: + - "9000:80" + + db: + container_name: db + image: postgres:14.1-alpine + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + ports: + - '5432:5432' + volumes: + - db:/var/lib/postgresql/data + + +volumes: + config: + data: + log: + db: + +networks: + default: + name: mqtt + external: true \ No newline at end of file diff --git a/gateway-classification/go.mod b/gateway-classification/go.mod new file mode 100644 index 0000000..1db4231 --- /dev/null +++ b/gateway-classification/go.mod @@ -0,0 +1,37 @@ +module github.com/stanleydv12/gateway-classification + +go 1.21.3 + +require ( + github.com/eclipse/paho.mqtt.golang v1.4.3 + github.com/galeone/tensorflow/tensorflow/go v0.0.0-20221023090153-6b7fa0680c3e + github.com/galeone/tfgo v0.0.0-20230715013254-16113111dc99 + github.com/joho/godotenv v1.5.1 + github.com/mitchellh/mapstructure v1.5.0 +) + +require ( + github.com/chzyer/readline v1.5.1 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20240205174729-1f824a1a9b87 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.4.3 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/tensorflow/tensorflow v2.15.0+incompatible // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.17.0 // indirect +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gorm.io/driver/postgres v1.5.4 + gorm.io/gorm v1.25.5 +) diff --git a/gateway-classification/go.sum b/gateway-classification/go.sum new file mode 100644 index 0000000..10c9ea0 --- /dev/null +++ b/gateway-classification/go.sum @@ -0,0 +1,69 @@ +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= +github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= +github.com/galeone/tensorflow/tensorflow/go v0.0.0-20221023090153-6b7fa0680c3e h1:9+2AEFZymTi25FIIcDwuzcOPH04z9+fV6XeLiGORPDI= +github.com/galeone/tensorflow/tensorflow/go v0.0.0-20221023090153-6b7fa0680c3e/go.mod h1:TelZuq26kz2jysARBwOrTv16629hyUsHmIoj54QqyFo= +github.com/galeone/tfgo v0.0.0-20230715013254-16113111dc99 h1:8Bt1P/zy1gb37L4n8CGgp1qmFwBV5729kxVfj0sqhJk= +github.com/galeone/tfgo v0.0.0-20230715013254-16113111dc99/go.mod h1:3YgYBeIX42t83uP27Bd4bSMxTnQhSbxl0pYSkCDB1tc= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo= +github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/ianlancetaylor/demangle v0.0.0-20240205174729-1f824a1a9b87 h1:IG+58MeF0K/wSXknV4FMOABt6B4VZCguRDxYSCJqWb4= +github.com/ianlancetaylor/demangle v0.0.0-20240205174729-1f824a1a9b87/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tensorflow/tensorflow v2.15.0+incompatible h1:qWaan+jC2rrHctJliI+TSAwLGdCoQdsrwN7YddLv81Y= +github.com/tensorflow/tensorflow v2.15.0+incompatible/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/gateway-classification/main.go b/gateway-classification/main.go new file mode 100644 index 0000000..170e20d --- /dev/null +++ b/gateway-classification/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "github.com/stanleydv12/gateway-classification/src/database" + "github.com/stanleydv12/gateway-classification/src/handler" + "github.com/stanleydv12/gateway-classification/src/mqtt" +) + +// main initializes the database and MQTT setup, then starts listening for MQTT messages. +// +// No parameters. +// No return types. +func main() { + // Setup Database + db := database.SetupDatabase() + handler.SetDBInstance(db) + + // Setup Mqtt + mqtt.SetupMqtt() + + // Application set to listen + mqtt.Sub(mqtt.Client) + + handler.StartTimer() + + // Block the main function with a select statement + select {} +} diff --git a/gateway-classification/mqtt/config/mosquitto.conf b/gateway-classification/mqtt/config/mosquitto.conf new file mode 100644 index 0000000..7e1ceef --- /dev/null +++ b/gateway-classification/mqtt/config/mosquitto.conf @@ -0,0 +1,7 @@ +persistence true +persistence_location /mosquitto/data/ +log_dest file /mosquitto/log/mosquitto.log +allow_anonymous true +listener 1883 0.0.0.0 +listener 9001 0.0.0.0 +protocol websockets diff --git a/gateway-classification/readme.md b/gateway-classification/readme.md new file mode 100644 index 0000000..4f824f4 --- /dev/null +++ b/gateway-classification/readme.md @@ -0,0 +1,27 @@ +# Panduan Setup MQTT + +Berikut adalah langkah-langkah untuk melakukan setup pada MQTT: +1. Buatlah folder mqtt yang didalamnya terdapat 3 folder lain yaitu config, data, dan log. +2. Lalu konfigurasi mqtt sesuai kebutuhan pada folder mqtt -> config -> mosquitto.conf + +# Panduan Menjalankan Dockerfile + +Berikut adalah langkah-langkah untuk menjalankan Dockerfile dan membangun image Docker: + +## Langkah 1: Build Docker Image + +```bash +docker build -t myapp . +``` + +## Langkah 2: Kirim Image ke Registry Cloud +```bash +docker login +docker tag : //: +``` + +# Panduan Testing + +Berikut adalah langkah-langkah untuk melakukan testing menggunakan JMeter: +1. Tambahkan plugin MQTT yang terdapat di folder infrastructure pada JMeter. +2. Lalu open file dengan tipe .jmx pada folder infrastructure tersebut. diff --git a/gateway-classification/src/classify/complex.go b/gateway-classification/src/classify/complex.go new file mode 100644 index 0000000..95a9552 --- /dev/null +++ b/gateway-classification/src/classify/complex.go @@ -0,0 +1,112 @@ +package classify + +import ( + "fmt" + "math" +) + +type Complex struct { + Re float64 // the real part + Im float64 // the imaginary part +} + +// create a new object with the given real and imaginary parts +func NewComplex(real, imag float64) Complex { + return Complex{Re: real, Im: imag} +} + +// return a string representation of the invoking Complex object +func (c Complex) String() string { + if c.Im == 0 { + return fmt.Sprintf("%g", c.Re) + } + if c.Re == 0 { + return fmt.Sprintf("%gi", c.Im) + } + if c.Im < 0 { + return fmt.Sprintf("%g - %gi", c.Re, -c.Im) + } + return fmt.Sprintf("%g + %gi", c.Re, c.Im) +} + +// return abs/modulus/magnitude +func (c Complex) Abs() float64 { + return math.Hypot(c.Re, c.Im) +} + +// return a new Complex object whose value is (this + b) +func (c Complex) Plus(b Complex) Complex { + return Complex{c.Re + b.Re, c.Im + b.Im} +} + +// return a new Complex object whose value is (this - b) +func (c Complex) Minus(b Complex) Complex { + return Complex{c.Re - b.Re, c.Im - b.Im} +} + +// return a new Complex object whose value is (this * b) +func (c Complex) Times(b Complex) Complex { + return Complex{c.Re*b.Re - c.Im*b.Im, c.Re*b.Im + c.Im*b.Re} +} + +// return a new Complex object whose value is the reciprocal of this +func (c Complex) Reciprocal() Complex { + scale := c.Re*c.Re + c.Im*c.Im + return Complex{c.Re / scale, -c.Im / scale} +} + +// return a / b +func (c Complex) Divides(b Complex) Complex { + return c.Times(b.Reciprocal()) +} + +// return a new Complex object whose value is the complex sine of this +func (c Complex) Sin() Complex { + return Complex{math.Sin(c.Re) * math.Cosh(c.Im), math.Cos(c.Re) * math.Sinh(c.Im)} +} + +// return a new Complex object whose value is the complex cosine of this +func (c Complex) Cos() Complex { + return Complex{math.Cos(c.Re) * math.Cosh(c.Im), -math.Sin(c.Re) * math.Sinh(c.Im)} +} + +// equals returns true if the given Complex object is equal to the receiver +func (c Complex) Equals(x Complex) bool { + return c.Re == x.Re && c.Im == x.Im +} + +// FFT computes the FFT of a complex sequence x[] of length n. +func FFT(x []Complex) []Complex { + n := len(x) + if n == 1 { + return []Complex{x[0]} + } + + if n%2 != 0 { + panic("n is not a power of 2") + } + + // Compute FFT of even terms + even := make([]Complex, n/2) + for k := 0; k < n/2; k++ { + even[k] = x[2*k] + } + q := FFT(even) + + // Compute FFT of odd terms + odd := even // Reuse the array + for k := 0; k < n/2; k++ { + odd[k] = x[2*k+1] + } + r := FFT(odd) + + // Combine + y := make([]Complex, n) + for k := 0; k < n/2; k++ { + kth := -2 * math.Pi * float64(k) / float64(n) + wk := Complex{math.Cos(kth), math.Sin(kth)} + y[k] = q[k].Plus(wk.Times(r[k])) + y[k+n/2] = q[k].Minus(wk.Times(r[k])) + } + return y +} \ No newline at end of file diff --git a/gateway-classification/src/classify/elmClassifier.go b/gateway-classification/src/classify/elmClassifier.go new file mode 100644 index 0000000..f41f72d --- /dev/null +++ b/gateway-classification/src/classify/elmClassifier.go @@ -0,0 +1 @@ +package classify \ No newline at end of file diff --git a/gateway-classification/src/classify/elmModel.go b/gateway-classification/src/classify/elmModel.go new file mode 100644 index 0000000..d938952 --- /dev/null +++ b/gateway-classification/src/classify/elmModel.go @@ -0,0 +1,96 @@ +package classify + +import ( + "errors" + "io/ioutil" + "strconv" + "strings" +) + +type RealMatrix struct { + Rows int + Cols int + Data [][]float64 +} + +type Weight struct { + W RealMatrix + BW RealMatrix +} + +type ELMModel struct { + InputWeight RealMatrix + BiasInputWeight RealMatrix + OutputWeight RealMatrix + Separator string +} + +func NewELMModel(inputWeightFilePath, outputWeightFilePath string) (*ELMModel, error) { + var elmModel ELMModel + + inputWeightFileBytes, err := ioutil.ReadFile(inputWeightFilePath) + if err != nil { + return nil, err + } + + outputWeightFileBytes, err := ioutil.ReadFile(outputWeightFilePath) + if err != nil { + return nil, err + } + + inputWeightFileLines := strings.Split(string(inputWeightFileBytes), "\n") + outputWeightFileLines := strings.Split(string(outputWeightFileBytes), "\n") + + weight, err := convertListCsvTo2dArr(inputWeightFileLines, true) + if err != nil { + return nil, err + } + elmModel.InputWeight = weight.W + elmModel.BiasInputWeight = weight.BW + + weight, err = convertListCsvTo2dArr(outputWeightFileLines, false) + if err != nil { + return nil, err + } + elmModel.OutputWeight = weight.W + + return &elmModel, nil +} + +func convertListCsvTo2dArr(input []string, useBias bool) (Weight, error) { + var weight Weight + + listSize := len(input) + if listSize == 0 { + return weight, errors.New("empty input list") + } + + csvElementSize := len(strings.Split(input[0], ",")) + for _, line := range input { + if len(strings.Split(line, ",")) != csvElementSize { + return weight, errors.New("invalid CSV length") + } + } + + weightData := make([][]float64, listSize) + biasWeightData := make([][]float64, listSize) + + for i, line := range input { + splittedLine := strings.Split(line, ",") + temp := make([]float64, len(splittedLine)) + for j, val := range splittedLine { + temp[j], _ = strconv.ParseFloat(strings.TrimSpace(val), 64) + } + if useBias { + weightData[i] = temp[:len(temp)-1] + biasWeightData[i] = []float64{temp[len(temp)-1]} + } else { + weightData[i] = temp + } + } + + weight.W = RealMatrix{Rows: listSize, Cols: len(weightData[0]), Data: weightData} + weight.BW = RealMatrix{Rows: listSize, Cols: 1, Data: biasWeightData} + + return weight, nil +} diff --git a/gateway-classification/src/classify/hrvFeature.go b/gateway-classification/src/classify/hrvFeature.go new file mode 100644 index 0000000..ea98e52 --- /dev/null +++ b/gateway-classification/src/classify/hrvFeature.go @@ -0,0 +1,255 @@ +package classify + +import ( + "math" + "sort" +) + +type HRVFeature struct { + F01_AVNN float64 + F02_SDNN float64 + F03_RMSSD float64 + F04_SDSD float64 + F05_NNx float64 + F06_PNNx float64 + F07_HRV_TRIANGULAR_IDX float64 + F08_SD1 float64 + F09_SD2 float64 + F10_SD1_SD2_RATIO float64 + F11_S float64 + F12_TP float64 + F13_pLF float64 + F14_pHF float64 + F15_LFHFratio float64 + F16_VLF float64 + F17_LF float64 + F18_HF float64 +} + +func NewHRVFeature(rrIntervalSet RRIntervalSet) *HRVFeature { + var hrv HRVFeature + + rrIntervalValue := rrIntervalSet.RRIntervalValue + rrIntervalsValueDiff := rrIntervalSet.RRIntervalsValueDiff + + hrv.F01_AVNN = f01_AVNN(rrIntervalValue) + hrv.F02_SDNN = f02_SDNN(rrIntervalValue) + hrv.F03_RMSSD = f03_RMSSD(rrIntervalsValueDiff) + hrv.F04_SDSD = f04_SDSD(rrIntervalsValueDiff) + hrv.F05_NNx = f05_NNx(rrIntervalsValueDiff, 50) + hrv.F06_PNNx = f06_PNNx(rrIntervalValue, hrv.F05_NNx) + hrv.F07_HRV_TRIANGULAR_IDX = f07_HRV_TRIANGULAR_IDX(rrIntervalValue) + hrv.F08_SD1 = f08_SD1(hrv.F04_SDSD) + hrv.F09_SD2 = f09_SD2(hrv.F02_SDNN, hrv.F04_SDSD) + hrv.F10_SD1_SD2_RATIO = f10_SD1_SD2_RATIO(hrv.F08_SD1, hrv.F09_SD2) + hrv.F11_S = f11_S(hrv.F08_SD1, hrv.F09_SD2) + + feature12To18 := f12_18(rrIntervalValue, 2) + hrv.F12_TP = feature12To18.TP + hrv.F13_pLF = feature12To18.pLF + hrv.F14_pHF = feature12To18.pHF + hrv.F15_LFHFratio = feature12To18.LFHFratio + hrv.F16_VLF = feature12To18.VLF + hrv.F17_LF = feature12To18.LF + hrv.F18_HF = feature12To18.HF + + return &hrv +} + +func f01_AVNN(rrIntervalValue []float64) float64 { + return mean(rrIntervalValue) +} + +func f02_SDNN(rrIntervalValue []float64) float64 { + return sampleStandardDeviation(rrIntervalValue) +} + +func f03_RMSSD(rrIntervalsValueDiff []float64) float64 { + return math.Sqrt(mean(powList(rrIntervalsValueDiff, 2))) +} + +func f04_SDSD(rrIntervalsValueDiff []float64) float64 { + return sampleStandardDeviation(rrIntervalsValueDiff) +} + +func f05_NNx(rrIntervalsValueDiff []float64, x float64) float64 { + count := filterCount(mulList(rrIntervalsValueDiff, 1000), func(y float64) bool { + return y > x + }) + return float64(count) +} + +func f06_PNNx(rrIntervalValue []float64, NNx float64) float64 { + return (NNx / (float64(len(rrIntervalValue)) - 1)) * 100 +} + +func f07_HRV_TRIANGULAR_IDX(rrIntervalValue []float64) float64 { + binSize := 7.812 + var tempRr []float64 + for _, val := range rrIntervalValue { + tempRr = append(tempRr, val*1000) + } + + sort.Float64s(tempRr) + maxVal := tempRr[len(tempRr)-1] + minVal := tempRr[0] + + binCount := math.Ceil((maxVal - minVal) / binSize) + edges := make([]float64, int(binCount)+1) + var Nds []float64 + edges[0] = minVal + + for i := 1; i <= int(binCount); i++ { + edges[i] = edges[i-1] + binSize + var d float64 + for _, x := range tempRr { + if x >= edges[i-1] && x < edges[i] { + d++ + } + } + if d != 0 { + Nds = append(Nds, d) + } + } + + return max(Nds) / sum(Nds) +} + +func f08_SD1(sdsd float64) float64 { + return math.Sqrt(math.Pow(sdsd, 2) / 2) +} + +func f09_SD2(sdnn, sdsd float64) float64 { + return math.Sqrt(2*math.Pow(sdnn, 2) - math.Pow(sdsd, 2)/2) +} + +func f10_SD1_SD2_RATIO(sd1, sd2 float64) float64 { + return sd1 / sd2 +} + +func f11_S(sd1, sd2 float64) float64 { + return math.Pi * sd1 * sd2 +} + +func f12_18(rrIntervalValue []float64, Fs float64) Feature12To18 { + var feature Feature12To18 + // Implement the logic for feature calculation + return feature +} + +func filterF(YY []float64, f []float64, predicate func(float64) bool) []float64 { + var result []float64 + for i, val := range f { + if predicate(val) { + result = append(result, YY[i]) + } + } + return result +} + +func nanzscore(input []float64) []float64 { + m := nanmean(input) + s := nanstd(input) + + var z []float64 + for _, val := range input { + z = append(z, (val-m)/s) + } + return z +} + +func nanmean(input []float64) float64 { + return mean(input) +} + +func nanstd(input []float64) float64 { + return populationStandardDeviation(input) +} + +type Feature12To18 struct { + TP float64 + pLF float64 + pHF float64 + LFHFratio float64 + VLF float64 + LF float64 + HF float64 +} + +type RRIntervalSet struct { + RRIntervalValue []float64 + RRIntervalsValueDiff []float64 +} + +func mean(arr []float64) float64 { + sum := 0.0 + for _, val := range arr { + sum += val + } + return sum / float64(len(arr)) +} + +func sampleStandardDeviation(arr []float64) float64 { + mean := mean(arr) + variance := 0.0 + for _, val := range arr { + variance += math.Pow(val-mean, 2) + } + return math.Sqrt(variance / float64(len(arr)-1)) +} + +func powList(arr []float64, exp float64) []float64 { + var result []float64 + for _, val := range arr { + result = append(result, math.Pow(val, exp)) + } + return result +} + +func filterCount(arr []float64, predicate func(float64) bool) int { + count := 0 + for _, val := range arr { + if predicate(val) { + count++ + } + } + return count +} + +func sum(arr []float64) float64 { + total := 0.0 + for _, val := range arr { + total += val + } + return total +} + +func max(arr []float64) float64 { + if len(arr) == 0 { + return 0 + } + max := arr[0] + for _, val := range arr { + if val > max { + max = val + } + } + return max +} + +func populationStandardDeviation(input []float64) float64 { + meanVal := mean(input) + variance := 0.0 + for _, val := range input { + variance += math.Pow(val-meanVal, 2) + } + return math.Sqrt(variance / float64(len(input))) +} + +func mulList(input []float64, multiplier float64) []float64 { + result := make([]float64, len(input)) + for i, val := range input { + result[i] = val * multiplier + } + return result +} diff --git a/gateway-classification/src/database/database.go b/gateway-classification/src/database/database.go new file mode 100644 index 0000000..6111134 --- /dev/null +++ b/gateway-classification/src/database/database.go @@ -0,0 +1,111 @@ +package database + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/stanleydv12/gateway-classification/src/entity" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// SetupDatabase initializes and returns a *gorm.DB object representing a connection to the database. +// +// It reads the database configuration from the environment variables and establishes a connection to the database using the specified parameters. +// The function automatically migrates the entity.SensorData struct to the database and returns the *gorm.DB object. +func SetupDatabase() *gorm.DB { + + var err error + + errEnv := godotenv.Load(".env") + + if errEnv != nil { + log.Fatal("Error loading .env") + } + + host := os.Getenv("DSN_HOST") + user := os.Getenv("DSN_USER") + password := os.Getenv("DSN_PASSWORD") + dbname := os.Getenv("DSN_DB_NAME") + port := os.Getenv("DSN_PORT") + sslMode := os.Getenv("SSL_MODE") + timeZone := os.Getenv("TIMEZONE") + + dsn := + "host=" + host + + " user=" + user + + " password=" + password + + " dbname=" + dbname + + " port=" + port + + " sslmode=" + sslMode + + " TimeZone=" + timeZone + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatalf("Error connecting to the database: %v", err) + } + + err = AutoMigrateAllEntities(db) + if err != nil { + log.Fatalf("Error migrating entities: %v", err) + } + + // Seeding + InsertTestData(db) + + return db +} + +// AutoMigrateAllEntities migrates all entities to the database. +func AutoMigrateAllEntities(db *gorm.DB) error { + err := db.AutoMigrate( + &entity.Doctor{}, + &entity.ECG{}, + &entity.Patient{}, + &entity.SleepData{}, + &entity.SleepStage{}, + &entity.SleepQuality{}, + ) + if err != nil { + return err + } + return nil +} + +func InsertTestData(db *gorm.DB) { + // Contoh data Doctor + doctorData := entity.Doctor{ + FirstName: "John", + LastName: "Doe", + Gender: "Male", + Email: "john.doe@example.com", + PhoneNumber: "123456789", + BirthRate: time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC), + RegistrationDate: time.Now(), + Address: "123 Main St, City", + } + + // Contoh data Patient + patientData := entity.Patient{ + FirstName: "Alice", + LastName: "Johnson", + Gender: "Female", + Email: "alice.johnson@example.com", + PhoneNumber: "987654321", + BirthDate: time.Date(1990, 5, 15, 0, 0, 0, 0, time.UTC), + RegistrationDate: time.Now(), + Address: "456 Oak St, Town", + SensorToken: "sensortoken123", + StartSleepTime: time.Now(), + EndSleepTime: time.Now().Add(8 * time.Hour), // Contoh: tidur selama 8 jam + } + + // Sisipkan data ke database + db.Create(&doctorData) + db.Create(&patientData) + + fmt.Println("Data inserted successfully.") +} diff --git a/gateway-classification/src/entity/entity.go b/gateway-classification/src/entity/entity.go new file mode 100644 index 0000000..3b8915c --- /dev/null +++ b/gateway-classification/src/entity/entity.go @@ -0,0 +1,76 @@ +package entity + +import ( + "time" +) + +// Doctor represents the Doctor table +type Doctor struct { + ID uint `gorm:"primaryKey" json:"id"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Gender string `json:"gender,omitempty"` + Email string `json:"email,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + BirthRate time.Time `json:"birth_rate,omitempty"` + RegistrationDate time.Time `json:"registration_date,omitempty"` + Address string `json:"address,omitempty"` +} + +// Patient represents the Patient table +type Patient struct { + ID uint `gorm:"primaryKey" json:"id"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Gender string `json:"gender,omitempty"` + Email string `json:"email,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + BirthDate time.Time `json:"birth_date,omitempty"` + RegistrationDate time.Time `json:"registration_date,omitempty"` + Address string `json:"address,omitempty"` + SensorToken string `json:"sensor_token,omitempty"` + StartSleepTime time.Time `json:"start_sleep_time,omitempty"` + EndSleepTime time.Time `json:"end_sleep_time,omitempty"` +} + +// SleepData represents the Sleep Data table +type SleepData struct { + ID uint `gorm:"primaryKey" json:"id"` + PatientID uint `json:"patient_id,omitempty"` + FirstECGID uint `json:"first_ecg_id,omitempty"` + FirstSleepStageID uint `json:"first_sleep_stage_id,omitempty"` + SleepQualityID uint `json:"sleep_quality_id,omitempty"` + FirstInputTime time.Time `json:"first_input_time,omitempty"` + LastInputTime time.Time `json:"last_input_time,omitempty"` +} + +// ECG represents the ECG table +type ECG struct { + ID uint `gorm:"primaryKey" json:"id"` + ReferenceID uint `json:"reference_id,omitempty"` + Value float64 `json:"value,omitempty"` + InputTime time.Time `json:"input_time,omitempty"` +} + +// SleepStage represents the Sleep Stage table +type SleepStage struct { + ID uint `gorm:"primaryKey" json:"id"` + ReferenceID uint `json:"reference_id,omitempty"` + Value string `json:"value,omitempty"` + Method string `json:"method,omitempty"` +} + +// SleepQuality represents the Sleep Quality table +type SleepQuality struct { + ID uint `gorm:"primaryKey" json:"id"` + Value string `json:"value,omitempty"` + InBedDuration time.Time `json:"in_bed_duration,omitempty"` + BeginToSleepTime time.Time `json:"begin_to_sleep_time,omitempty"` + AwakeFromSleepTime time.Time `json:"awake_from_sleep_time,omitempty"` + SleepEfficiency float64 `json:"sleep_efficiency,omitempty"` + LightSleepDuration time.Time `json:"light_sleep_duration,omitempty"` + DeepSleepDuration time.Time `json:"deep_sleep_duration,omitempty"` + REMDuration time.Time `json:"rem_duration,omitempty"` + AwakeDuration time.Time `json:"awake_duration,omitempty"` + InputTime time.Time `json:"input_time,omitempty"` +} diff --git a/gateway-classification/src/handler/handler.go b/gateway-classification/src/handler/handler.go new file mode 100644 index 0000000..25acc29 --- /dev/null +++ b/gateway-classification/src/handler/handler.go @@ -0,0 +1,353 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/stanleydv12/gateway-classification/src/entity" + "gorm.io/gorm" +) + +var saveTimer *time.Timer + +// HandleEvent handles the given event and data. +// +// Parameters: +// - event: a string representing the event to be handled. +// - data: an interface{} containing the data related to the event. +// +// Returns: None. +func HandleEvent(event string, data interface{}) { + switch event { + case "save-data": + sensorData, ok := convertToSensorData(data) + if !ok { + fmt.Println("HandleEvent: Failed to convert data to SensorData") + return + } + SaveData(sensorData) + default: + fmt.Println("HandleEvent: unknown event") + } +} + +// SaveData saves the provided data to the database if it meets certain conditions. +// +// The function takes in a data interface{} parameter, which should be of type entity.ECG. +// It first checks if the data is of the correct type using type assertion. +// If the data is not of type entity.ECG, the function prints an error message and returns. +// +// The function then parses the input timestamp of the data and checks if it is valid. +// If there is an error in parsing the timestamp, the function prints an error message and returns. +// +// The function calculates the time difference between the current time and the input timestamp. +// If the time difference is greater than 30 seconds, the function prints a message and returns. +// +// If the data is the first ECG for a patient, the function sets the ReferenceID to 0. +// Otherwise, it retrieves the ReferenceID for the patient using the GetReferenceIDForPatient function. +// +// The function creates a new instance of entity.ECG with the updated ReferenceID and other attributes. +// +// The function then attempts to save the newSensorData to the database using the DB.Create function. +// If there is an error in saving the data, the function prints an error message and returns. +// +// Finally, if the data is successfully saved to the database, the function prints a success message. +func SaveData(data interface{}) { + fmt.Println(data) + sensorData, ok := data.(entity.ECG) + if !ok { + fmt.Println("SaveData: Invalid data format") + return + } + + // Note: Inject input time with current time + sensorData.InputTime = time.Now() + currentTime := time.Now() + + diff := currentTime.Sub(sensorData.InputTime) + if diff.Seconds() > 30 { + fmt.Println("SaveData: Data is older than 30 seconds, skipping save to DB") + return + } + + if isFirstEcg := CheckIfFirstECG(sensorData); isFirstEcg { + sensorData.ReferenceID = 0 + + newSensorData := entity.ECG{ + ReferenceID: sensorData.ReferenceID, + Value: sensorData.Value, + InputTime: sensorData.InputTime, + } + + err := DB.Create(&newSensorData).Error + if err != nil { + fmt.Println("SaveData: Failed to save data to DB:", err) + return + } + + firstSensorData := entity.ECG{} + DB.Select("id").First(&firstSensorData) + + sleepData := entity.SleepData{ + ID: 1, + PatientID: 1, + FirstECGID: firstSensorData.ID, + FirstInputTime: firstSensorData.InputTime, + } + + err = DB.Save(&sleepData).Error + if err != nil { + fmt.Println("SaveData: Failed to save sleep data to DB:", err) + return + } + + } else { + sensorData.ReferenceID = GetReferenceIDForPatient(sensorData) + + newSensorData := entity.ECG{ + ReferenceID: sensorData.ReferenceID, + Value: sensorData.Value, + InputTime: sensorData.InputTime, + } + + err := DB.Create(&newSensorData).Error + if err != nil { + fmt.Println("SaveData: Failed to save data to DB:", err) + return + } + + sleepData := entity.SleepData{ + ID: 1, + PatientID: 1, + LastInputTime: sensorData.InputTime, + } + + err = DB.Save(&sleepData).Error + if err != nil { + fmt.Println("SaveData: Failed to save sleep data to DB:", err) + return + } + } + + // Reset the timer to 30 seconds + if saveTimer == nil { + saveTimer = time.NewTimer(30 * time.Second) + } else { + saveTimer.Reset(30 * time.Second) + } + + fmt.Println("SaveData: Data saved to DB") +} + +// convertToSensorData converts the given data to a SensorData object. +// +// data: The data to be converted. +// Returns: The converted SensorData object and a boolean indicating if the conversion was successful. +func convertToSensorData(data interface{}) (entity.ECG, bool) { + var sensorData entity.ECG + err := mapstructure.Decode(data, &sensorData) + if err != nil { + return entity.ECG{}, false + } + return sensorData, true +} + +var DB *gorm.DB + +// SetDBInstance sets the global variable DB to the given *gorm.DB instance. +// +// db: the *gorm.DB instance to be set. +func SetDBInstance(db *gorm.DB) { + DB = db +} + +// CheckIfFirstECG checks if the given ECG sensor data is the first data recorded for a given date. +// +// It takes an ECG object as a parameter and returns a boolean value indicating whether the data is the first for that date. +func CheckIfFirstECG(sensorData entity.ECG) bool { + var count int64 + var lastData entity.ECG + + // Check if data is empty + DB.Model(&entity.ECG{}).Count(&count) + if count == 0 { + return true + } + + // Check if the last data for reference_id is not empty + DB.Order("input_time DESC").Where("reference_id = ?", sensorData.ReferenceID).First(&lastData) + if lastData.ID != 0 { + // Check if input_time is more than 30 seconds from the current time + currentTime := time.Now() + timeDiff := currentTime.Sub(lastData.InputTime) + if timeDiff.Seconds() > 30 { + return true + } + } + + return false +} + +// GetReferenceIDForPatient retrieves the reference ID for a patient based on the given ECG sensor data. +// +// It takes the following parameter: +// - sensorData: an instance of the entity.ECG struct representing the ECG sensor data. +// +// It returns a uint representing the reference ID. +func GetReferenceIDForPatient(sensorData entity.ECG) uint { + var referenceID uint + DB.Model(&entity.ECG{}).Where("DATE(input_time) = ?", sensorData.InputTime.Format("2006-01-02")).Select("id").Order("id ASC").First(&referenceID) + return referenceID +} + +// StartTimer starts the timer for saving data to DB and calling classifyData +func StartTimer() { + saveTimer = time.NewTimer(30 * time.Second) + + go func() { + for { + select { + case <-saveTimer.C: + classifyData() + saveTimer.Reset(30 * time.Second) + } + } + }() +} + +type PredictionResponse struct { + Prediction string `json:"prediction"` +} + +func classifyData() { + + var lastECG entity.ECG + if err := DB.Order("input_time desc").First(&lastECG).Error; err != nil { + if err == gorm.ErrRecordNotFound { + fmt.Println("No ecg data found in the database") + } else { + log.Fatalf("Failed to get last ecg data: %v", err) + } + return + } + + sleepData := entity.SleepData{ + ID: 1, + LastInputTime: lastECG.InputTime, + } + if err := DB.Save(&sleepData).Error; err != nil { + log.Fatalf("Failed to save sleep data: %v", err) + return + } + + + // Mengambil data ECG berdasarkan ID + var ecgByID []entity.ECG + if err := DB.Where("id = ?", lastECG.ReferenceID).Find(&ecgByID).Error; err != nil { + log.Fatalf("Failed to get ECG data by ID: %v", err) + return + } + + // Mengambil semua data ECG berdasarkan reference ID + var ecgByReferenceID []entity.ECG + if err := DB.Where("reference_id = ?", lastECG.ReferenceID).Find(&ecgByReferenceID).Error; err != nil { + log.Fatalf("Failed to get ECG data by reference ID: %v", err) + return + } + + // Combine data retrieved by ID and reference ID + allECG := append(ecgByID, ecgByReferenceID...) + + // Process data in batches of 10 + for i := 0; i < len(allECG); i += 10 { + end := i + 10 + if end > len(allECG) { + end = len(allECG) + } + + batch := allECG[i:end] + + // Call the function to handle the API call for this batch + prediction, err := predict(batch) + if err != nil { + log.Fatalf("Failed to make prediction: %v", err) + return + } + + if i == 0 { + sleepStage := entity.SleepStage{ + ID: 1, + ReferenceID: 0, + Value: prediction.Prediction, + } + + err = DB.Save(&sleepStage).Error + if err != nil { + log.Fatalf("Failed to save sleep stage: %v", err) + return + } + + var sleepData entity.SleepData + err = DB.Find(&sleepData, "id = ?", 1).First(&sleepData).Error + if err != nil { + log.Fatalf("Failed to get sleep data: %v", err) + return + } + + sleepData.FirstSleepStageID = sleepStage.ID + } else { + sleepStage := entity.SleepStage{ + ReferenceID: 1, + Value: prediction.Prediction, + } + + err = DB.Save(&sleepStage).Error + if err != nil { + log.Fatalf("Failed to save sleep stage: %v", err) + return + } + } + } +} + +func predict(data []entity.ECG) (PredictionResponse, error) { + extractedValues := make([]float64, len(data)) + + for i, ecg := range data { + extractedValues[i] = ecg.Value + } + + // Convert the extracted data slice to JSON + jsonData, err := json.Marshal(data) + if err != nil { + return PredictionResponse{}, err + } + + predictURL := os.Getenv("PREDICT_URL") + if predictURL == "" { + return PredictionResponse{}, fmt.Errorf("PREDICT_URL environment variable is not set") + } + + resp, err := http.Post(predictURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return PredictionResponse{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return PredictionResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var predictionResponse PredictionResponse + if err := json.NewDecoder(resp.Body).Decode(&predictionResponse); err != nil { + return PredictionResponse{}, err + } + + return predictionResponse, nil +} diff --git a/gateway-classification/src/mqtt/mqtt.go b/gateway-classification/src/mqtt/mqtt.go new file mode 100644 index 0000000..a103f47 --- /dev/null +++ b/gateway-classification/src/mqtt/mqtt.go @@ -0,0 +1,96 @@ +package mqtt + +import ( + "encoding/json" + "fmt" + "log" + "os" + + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/joho/godotenv" + "github.com/stanleydv12/gateway-classification/src/handler" +) + +type Message struct { + Event string `json:"event"` + Data interface{} `json:"data"` +} + +var Client mqtt.Client + +var messagePubHandler mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) { + fmt.Printf("Message Received from [%s] : %s\n", msg.Topic(), msg.Payload()) + + var receivedMessage Message + err := json.Unmarshal(msg.Payload(), &receivedMessage) + if err != nil { + fmt.Println("Error decoding message:", err) + return + } + + handler.HandleEvent(receivedMessage.Event, receivedMessage.Data) +} + +var connectHandler mqtt.OnConnectHandler = func(client mqtt.Client) { + fmt.Println("Connected") + Sub(client) +} + +var onLostHandler mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) { + fmt.Printf("Connection lost : %v", err) +} + +// SetupMqtt sets up the MQTT connection. +// +// It loads the environment variables from the .env file. +// It creates a new MQTT client options object and configures it with the +// MQTT broker host, port, and client ID. +// It sets the default publish handler, on connect handler, and connection lost +// handler for the MQTT client options. +// It creates a new MQTT client with the configured options. +// It connects the MQTT client to the broker. +// If there is an error during the connection, it panics. +func SetupMqtt() { + errEnv := godotenv.Load(".env") + if errEnv != nil { + log.Fatal("Error loading .env") + } + + broker := os.Getenv("MQTT_BROKER_HOST") + port := os.Getenv("MQTT_BROKER_PORT") + clientID := os.Getenv("MQTT_CLIENT_ID") + + opts := mqtt.NewClientOptions() + opts.AddBroker(fmt.Sprintf("ws://%s:%s", broker, port)) + opts.SetClientID(clientID) + opts.SetDefaultPublishHandler(messagePubHandler) + opts.SetOnConnectHandler(connectHandler) + opts.SetConnectionLostHandler(onLostHandler) + + Client = mqtt.NewClient(opts) + + if token := Client.Connect(); token.Wait() && token.Error() != nil { + panic(token.Error()) + } +} + +// Sub is a function that subscribes to an MQTT topic and prints a message. +// +// It takes a client of type mqtt.Client as a parameter. +// It does not return any value. +func Sub(client mqtt.Client) { + topic := os.Getenv("MQTT_TOPIC") + token := client.Subscribe(topic, 1, nil) + token.Wait() + fmt.Printf("Subscribed to topic %s\n", topic) +} + +// Pub publishes the given message to the MQTT topic. +// +// It takes a single parameter, msg, which is of type interface{}. +// The function does not return any value. +func Pub(msg interface{}) { + topic := os.Getenv("MQTT_TOPIC") + token := Client.Publish(topic, 1, false, msg) + token.Wait() +} diff --git a/quantification/go.mod b/quantification/go.mod new file mode 100644 index 0000000..0836f80 --- /dev/null +++ b/quantification/go.mod @@ -0,0 +1,17 @@ +module github.com/dije07/sqqs + +go 1.21.3 + +require ( + github.com/eclipse/paho.mqtt.golang v1.4.3 + github.com/go-redis/redis/v8 v8.11.5 + github.com/lib/pq v1.10.9 +) + +require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gorilla/websocket v1.5.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sync v0.1.0 // indirect +) diff --git a/quantification/go.sum b/quantification/go.sum new file mode 100644 index 0000000..3a59eb8 --- /dev/null +++ b/quantification/go.sum @@ -0,0 +1,32 @@ +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= +github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/quantification/quantification.go b/quantification/quantification.go new file mode 100644 index 0000000..ccecedb --- /dev/null +++ b/quantification/quantification.go @@ -0,0 +1,390 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "math" + "os" + "time" + + "github.com/go-redis/redis/v8" + _ "github.com/lib/pq" + + MQTT "github.com/eclipse/paho.mqtt.golang" +) + +// Define your MQTT broker address and port +const ( + brokerAddress = "ws://localhost:9001" // Change this to your MQTT broker address + host = "localhost" + port = 5432 + user = "postgres" + password = "postgres" + dbname = "sleep_monitoring" +) + +var DB *sql.DB + +type Message struct { + Event string `json:"event"` + Data interface{} `json:"data"` +} + +type SleepQuality struct { + ID uint `gorm:"primaryKey" json:"id"` + Value string `json:"value,omitempty"` + DeepSleepDuration float64 `json:"deep_sleep_duration,omitempty"` + AwakeDuration float64 `json:"awake_duration,omitempty"` + TotalSleepDuration float64 `json:"total_sleep_duration,omitempty"` + InputTime time.Time `json:"input_time,omitempty"` + UserId uint `json:"user_id,omitempty"` +} + +// FuzzySet represents a fuzzy set with a membership function. +type FuzzySet struct { + Name string + Terms map[string]func(float64) float64 +} + +// Fuzzify function calculates the membership values for each term in the fuzzy set. +func (fs *FuzzySet) Fuzzify(value float64) string { + max := 0.0 + result := "" + + for term, mf := range fs.Terms { + membership := mf(value) + if membership > max { + max = membership + result = term + } + } + + return result +} + +// ApplyRule function evaluates the conditions and returns the corresponding action. +func ApplyRule(conditions map[string]float64) (string, bool) { + // Apply all rules + switch { + case conditions["awakeDuration"] == 1 && conditions["deepSleepTime"] == 1 && conditions["totalSleepTime"] == 1: + return "LEVEL 9", true + case conditions["awakeDuration"] == 1 && conditions["deepSleepTime"] == 1 && conditions["totalSleepTime"] == 0.5: + return "LEVEL 8", true + case conditions["awakeDuration"] == 1 && conditions["deepSleepTime"] == 1 && conditions["totalSleepTime"] == 0: + return "LEVEL 7", true + case conditions["awakeDuration"] == 1 && conditions["deepSleepTime"] == 0.5 && conditions["totalSleepTime"] == 1: + return "LEVEL 7", true + case conditions["awakeDuration"] == 1 && conditions["deepSleepTime"] == 0.5 && conditions["totalSleepTime"] == 0.5: + return "LEVEL 6", true + case conditions["awakeDuration"] == 1 && conditions["deepSleepTime"] == 0.5 && conditions["totalSleepTime"] == 0: + return "LEVEL 5", true + case conditions["awakeDuration"] == 1 && conditions["deepSleepTime"] == 0 && conditions["totalSleepTime"] == 1: + return "LEVEL 5", true + case conditions["awakeDuration"] == 1 && conditions["deepSleepTime"] == 0 && conditions["totalSleepTime"] == 0.5: + return "LEVEL 4", true + case conditions["awakeDuration"] == 1 && conditions["deepSleepTime"] == 0 && conditions["totalSleepTime"] == 0: + return "LEVEL 3", true + case conditions["awakeDuration"] == 0.5 && conditions["deepSleepTime"] == 1 && conditions["totalSleepTime"] == 1: + return "LEVEL 8", true + case conditions["awakeDuration"] == 0.5 && conditions["deepSleepTime"] == 1 && conditions["totalSleepTime"] == 0.5: + return "LEVEL 7", true + case conditions["awakeDuration"] == 0.5 && conditions["deepSleepTime"] == 1 && conditions["totalSleepTime"] == 0: + return "LEVEL 6", true + case conditions["awakeDuration"] == 0.5 && conditions["deepSleepTime"] == 0.5 && conditions["totalSleepTime"] == 1: + return "LEVEL 6", true + case conditions["awakeDuration"] == 0.5 && conditions["deepSleepTime"] == 0.5 && conditions["totalSleepTime"] == 0.5: + return "LEVEL 5", true + case conditions["awakeDuration"] == 0.5 && conditions["deepSleepTime"] == 0.5 && conditions["totalSleepTime"] == 0: + return "LEVEL 4", true + case conditions["awakeDuration"] == 0.5 && conditions["deepSleepTime"] == 0 && conditions["totalSleepTime"] == 1: + return "LEVEL 4", true + case conditions["awakeDuration"] == 0.5 && conditions["deepSleepTime"] == 0 && conditions["totalSleepTime"] == 0.5: + return "LEVEL 3", true + case conditions["awakeDuration"] == 0.5 && conditions["deepSleepTime"] == 0 && conditions["totalSleepTime"] == 0: + return "LEVEL 2", true + case conditions["awakeDuration"] == 0 && conditions["deepSleepTime"] == 1 && conditions["totalSleepTime"] == 1: + return "LEVEL 7", true + case conditions["awakeDuration"] == 0 && conditions["deepSleepTime"] == 1 && conditions["totalSleepTime"] == 0.5: + return "LEVEL 6", true + case conditions["awakeDuration"] == 0 && conditions["deepSleepTime"] == 1 && conditions["totalSleepTime"] == 0: + return "LEVEL 5", true + case conditions["awakeDuration"] == 0 && conditions["deepSleepTime"] == 0.5 && conditions["totalSleepTime"] == 1: + return "LEVEL 5", true + case conditions["awakeDuration"] == 0 && conditions["deepSleepTime"] == 0.5 && conditions["totalSleepTime"] == 0.5: + return "LEVEL 4", true + case conditions["awakeDuration"] == 0 && conditions["deepSleepTime"] == 0.5 && conditions["totalSleepTime"] == 0: + return "LEVEL 3", true + case conditions["awakeDuration"] == 0 && conditions["deepSleepTime"] == 0 && conditions["totalSleepTime"] == 1: + return "LEVEL 3", true + case conditions["awakeDuration"] == 0 && conditions["deepSleepTime"] == 0 && conditions["totalSleepTime"] == 0.5: + return "LEVEL 2", true + case conditions["awakeDuration"] == 0 && conditions["deepSleepTime"] == 0 && conditions["totalSleepTime"] == 0: + return "LEVEL 1", true + } + + // No rule matched + return "", false +} + +var messagePubHandler MQTT.MessageHandler = func(client MQTT.Client, msg MQTT.Message) { + fmt.Printf("Message Received from [%s] : %s\n", msg.Topic(), msg.Payload()) + + if string(msg.Payload()) == "sleepStageReady" { + quantifyData() + } +} + +func setUpDB() *sql.DB { + // Construct the connection string + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), + os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), + os.Getenv("DB_NAME")) + + // Open a connection to the database + db, err := sql.Open("postgres", connStr) + if err != nil { + log.Fatal("Error connecting to the database: ", err) + } + + // Attempt to ping the database to verify connection + err = db.Ping() + if err != nil { + log.Fatal("Error pinging the database: ", err) + } + + fmt.Println("Successfully connected to the database!") + + return db +} + +var red *redis.Client + +func quantifyData() { + + var awake float64 = 0 + var deepSleep float64 = 0 + var totalSleep float64 = 0 + + db := setUpDB() + DB = db + value, err := red.Get(red.Context(), "sleep-stages").Result() + if value == "" || err != nil { + // Example query + start := time.Now().UnixMilli() + rows, err := DB.Query("SELECT * FROM sleep_stages") + end := time.Now().UnixMilli() + fmt.Println("Start : ", start) + fmt.Println("End : ", end) + fmt.Println("Duration : ", end-start, "ms") + if err != nil { + log.Fatal("Error querying the database: ", err) + } + defer rows.Close() + + // Iterate over the rows + for rows.Next() { + var id int + var reference_id string + var value string + var method any + var timestamp time.Time + err := rows.Scan(&id, &reference_id, &value, &method, ×tamp) + if err != nil { + log.Println("Error scanning row: ", err) + } + if value == "N1" || value == "N2" || value == "REM" { + totalSleep++ + } + if value == "N3" { + totalSleep++ + deepSleep++ + } + if value == "AWAKE" { + awake++ + } + } + if err := rows.Err(); err != nil { + log.Println("Error iterating over rows: ", err) + } + } else { + type SleepStage struct { + ID uint `json:"id"` + ReferenceID uint `json:"reference_id"` + Value string `json:"value"` + Method string `json:"method"` + Timestamp time.Time `json:"timestamp"` + } + + type SleepStages struct { + Stages []SleepStage `json:"sleep-stages"` + } + + // Unmarshal the JSON into a SleepStages struct + var stages SleepStages + err := json.Unmarshal([]byte(value), &stages) + if err != nil { + log.Println(err) + } + + // Loop through the stages and print values + for _, stage := range stages.Stages { + if stage.Value == "N1" || stage.Value == "N2" || stage.Value == "REM" { + totalSleep++ + } + if stage.Value == "N3" { + totalSleep++ + deepSleep++ + } + if stage.Value == "AWAKE" { + awake++ + } + } + } + + // Define fuzzy sets and input values + awakeDurationSet := FuzzySet{ + Name: "awakeDuration", + Terms: map[string]func(float64) float64{ + "low": func(x float64) float64 { return math.Max(0, 1.0-0.5*x) }, + "medium": func(x float64) float64 { return math.Max(0, 1-2*math.Abs(x-0.5)) }, + "high": func(x float64) float64 { return math.Max(0, 2*x-1) }, + }, + } + deepSleepDurationSet := FuzzySet{ + Name: "awakeDuration", + Terms: map[string]func(float64) float64{ + "low": func(x float64) float64 { return math.Max(0, 1.0-0.5*x) }, + "medium": func(x float64) float64 { return math.Max(0, 1-2*math.Abs(x-0.5)) }, + "high": func(x float64) float64 { return math.Max(0, 2*x-1) }, + }, + } + totalSleepDurationSet := FuzzySet{ + Name: "awakeDuration", + Terms: map[string]func(float64) float64{ + "low": func(x float64) float64 { return math.Max(0, 1.0-0.5*x) }, + "medium": func(x float64) float64 { return math.Max(0, 1-2*math.Abs(x-0.5)) }, + "high": func(x float64) float64 { return math.Max(0, 2*x-1) }, + }, + } + + inputValues := map[string]float64{ + "awakeDuration": awake / 60, + "deepSleepTime": deepSleep / 60, + "totalSleepTime": totalSleep / 60, + } + + // Fuzzify the sleep data + awakeTerm := awakeDurationSet.Fuzzify(inputValues["awakeDuration"]) + deepSleepTerm := deepSleepDurationSet.Fuzzify(inputValues["deepSleepTime"]) + totalSleepTerm := totalSleepDurationSet.Fuzzify(inputValues["totalSleepTime"]) + // Fuzzify the other sleep data... + + // Print the intermediate results + fmt.Println("Fuzzy Conditions:") + fmt.Printf("Awake Duration: %s (%f)\n", awakeTerm, inputValues["awakeDuration"]) + fmt.Printf("Deep Sleep Duration: %s (%f)\n", deepSleepTerm, inputValues["deepSleepTime"]) + fmt.Printf("Total Sleep Duration: %s (%f)\n", totalSleepTerm, inputValues["totalSleepTime"]) + // Print the other fuzzy conditions... + + // Map the linguistic terms to their respective numerical values + linguisticValues := map[string]float64{ + "high": 1.0, + "medium": 0.5, + "low": 0.0, + } + + // Create a map for all conditions + allConditions := map[string]float64{ + "awakeDuration": linguisticValues[awakeTerm], + "deepSleepTime": linguisticValues[deepSleepTerm], + "totalSleepTime": linguisticValues[totalSleepTerm], + } + + action, ruleMatched := ApplyRule(allConditions) + + // Print the final results + if ruleMatched { + fmt.Println("\nRule Matched! Action:", action) + } else { + fmt.Println("\nNo Rule Matched.") + } + + sleepQuality := SleepQuality{ + Value: action, + DeepSleepDuration: inputValues["deepSleepTime"], + TotalSleepDuration: inputValues["totalSleepTime"], + AwakeDuration: inputValues["awakeDuration"], + InputTime: time.Now(), + UserId: 1, + } + + sleepQualityJSON, err := json.Marshal(sleepQuality) + if err != nil { + fmt.Println("Error:", err) + return + } + + err = red.Set(red.Context(), "sleep_quality_1", sleepQualityJSON, 0).Err() + if err != nil { + log.Fatalf("Failed to save to redis: %v", err) + } else { + fmt.Println("Saved to redis") + } + + // Prepare the SQL statement for inserting data. + stmt, err := db.Prepare("INSERT INTO sleep_qualities (value, deep_sleep_duration, awake_duration, total_sleep_duration, input_time, user_id) VALUES ($1, $2, $3, $4, $5, $6)") + if err != nil { + log.Fatal(err) + } + defer stmt.Close() + + // Example data to be inserted. + value1 := action + value2 := inputValues["deepSleepTime"] + value3 := inputValues["awakeDuration"] + value4 := inputValues["totalSleepTime"] + value5 := time.Now() + value6 := 1 + + // Execute the SQL statement with the provided values. + _, err = stmt.Exec(value1, value2, value3, value4, value5, value6) + if err != nil { + log.Println(err) + } + +} + +func main() { + red = redis.NewClient(&redis.Options{ + Addr: os.Getenv("REDIS_ADDR"), // Redis server address + Password: "", // no password set + DB: 0, // use default DB + }) + + // Set MQTT client options + + opts := MQTT.NewClientOptions().AddBroker("ws://" + os.Getenv("MQTT_BROKER")) + opts.SetClientID("MQTT_simulator") + opts.SetDefaultPublishHandler(messagePubHandler) + + topic := "sleep_monitoring" + + // Create and start a client using the above options + client := MQTT.NewClient(opts) + if token := client.Connect(); token.Wait() && token.Error() != nil { + fmt.Println("Error connecting to MQTT broker:", token.Error()) + os.Exit(1) + } + + // Subscribe to MQTT topics + if token := client.Subscribe(topic, 0, messagePubHandler); token.Wait() && token.Error() != nil { + fmt.Println("Error subscribing to MQTT topic:", token.Error()) + os.Exit(1) + } + fmt.Printf("Subscribed to topic: %s\n", topic) + // Keep the program running indefinitely + select {} +}