commit
20f364ecca
|
|
@ -5,6 +5,8 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"github.com/leandrofars/oktopus/internal/api"
|
||||
"github.com/leandrofars/oktopus/internal/db"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
|
@ -25,10 +27,9 @@ func main() {
|
|||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
log.Println("Starting Oktopus Project TR-369 Controller Version:", VERSION)
|
||||
//flBroker := flag.Bool("m", false, "Defines if mosquitto container must run or not")
|
||||
// fl_endpointId := flag.String("endpoint_id", "proto::oktopus-controller", "Defines the enpoint id the Agent must trust on.")
|
||||
flSubTopic := flag.String("s", "oktopus/+/agent/+", "That's the topic agent must publish to, and the controller keeps on listening.")
|
||||
// fl_pub_topic := flag.String("pub_topic", "oktopus/v1/controller", "That's the topic controller must publish to, and the agent keeps on listening.")
|
||||
flDevicesTopic := flag.String("d", "oktopus/devices", "That's the topic mqtt broker end new devices info.")
|
||||
flSubTopic := flag.String("sub", "oktopus/+/controller/+", "That's the topic agent must publish to, and the controller keeps on listening.")
|
||||
flBrokerAddr := flag.String("a", "localhost", "Mqtt broker adrress")
|
||||
flBrokerPort := flag.String("p", "1883", "Mqtt broker port")
|
||||
flTlsCert := flag.String("ca", "", "TLS ca certificate")
|
||||
|
|
@ -36,6 +37,8 @@ func main() {
|
|||
flBrokerPassword := flag.String("P", "", "Mqtt broker password")
|
||||
flBrokerClientId := flag.String("i", "", "A clientid for the Mqtt connection")
|
||||
flBrokerQos := flag.Int("q", 2, "Quality of service of mqtt messages delivery")
|
||||
flAddrDB := flag.String("mongo", "mongodb://localhost:27017/", "MongoDB URI")
|
||||
flApiPort := flag.String("ap", "8000", "Rest api port")
|
||||
flHelp := flag.Bool("help", false, "Help")
|
||||
|
||||
flag.Parse()
|
||||
|
|
@ -44,34 +47,35 @@ func main() {
|
|||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
//if *flBroker {
|
||||
// log.Println("Starting Mqtt Broker")
|
||||
// mqtt.StartMqttBroker()
|
||||
//}
|
||||
/*
|
||||
This context suppress our needs, but we can use a more sofisticate
|
||||
approach with cancel and timeout options passing it through paho mqtt functions.
|
||||
*/
|
||||
ctx := context.Background()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
database := db.NewDatabase(ctx, *flAddrDB)
|
||||
/*
|
||||
If you want to use another message protocol just make it implement Broker interface.
|
||||
*/
|
||||
mqttClient := mqtt.Mqtt{
|
||||
Addr: *flBrokerAddr,
|
||||
Port: *flBrokerPort,
|
||||
Id: *flBrokerClientId,
|
||||
User: *flBrokerUsername,
|
||||
Passwd: *flBrokerPassword,
|
||||
Ctx: ctx,
|
||||
QoS: *flBrokerQos,
|
||||
SubTopic: *flSubTopic,
|
||||
CA: *flTlsCert,
|
||||
Addr: *flBrokerAddr,
|
||||
Port: *flBrokerPort,
|
||||
Id: *flBrokerClientId,
|
||||
User: *flBrokerUsername,
|
||||
Passwd: *flBrokerPassword,
|
||||
Ctx: ctx,
|
||||
QoS: *flBrokerQos,
|
||||
SubTopic: *flSubTopic,
|
||||
DevicesTopic: *flDevicesTopic,
|
||||
CA: *flTlsCert,
|
||||
DB: database,
|
||||
}
|
||||
|
||||
mtp.MtpService(&mqttClient, done)
|
||||
a := api.NewApi(*flApiPort, database)
|
||||
api.StartApi(a)
|
||||
|
||||
<-done
|
||||
cancel()
|
||||
|
||||
log.Println("(⌐■_■) Oktopus is out!")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,23 @@ module github.com/leandrofars/oktopus
|
|||
|
||||
go 1.18
|
||||
|
||||
require google.golang.org/protobuf v1.28.1
|
||||
require (
|
||||
github.com/eclipse/paho.golang v0.10.0
|
||||
go.mongodb.org/mongo-driver v1.11.3
|
||||
google.golang.org/protobuf v1.28.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/eclipse/paho.golang v0.10.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.1 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.3 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,67 @@
|
|||
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.golang v0.10.0 h1:oUGPjRwWcZQRgDD9wVDV7y7i7yBSxts3vcvcNJo8B4Q=
|
||||
github.com/eclipse/paho.golang v0.10.0/go.mod h1:rhrV37IEwauUyx8FHrvmXOKo+QRKng5ncoN1vJiJMcs=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y=
|
||||
go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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=
|
||||
|
|
|
|||
63
backend/services/controller/internal/api/api.go
Normal file
63
backend/services/controller/internal/api/api.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/leandrofars/oktopus/internal/db"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Api struct {
|
||||
Port string
|
||||
Db db.Database
|
||||
}
|
||||
|
||||
func NewApi(port string, db db.Database) Api {
|
||||
return Api{
|
||||
Port: port,
|
||||
Db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func StartApi(a Api) {
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
})
|
||||
r.HandleFunc("/devices", a.retrieveDevices)
|
||||
//r.HandleFunc("/devices/{sn}", a.devicesMessaging)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: "0.0.0.0:" + a.Port,
|
||||
// Good practice to set timeouts to avoid Slowloris attacks.
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
Handler: r, // Pass our instance of gorilla/mux in.
|
||||
}
|
||||
|
||||
// Run our server in a goroutine so that it doesn't block.
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) {
|
||||
devices, err := a.Db.RetrieveDevices()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(devices)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
28
backend/services/controller/internal/db/db.go
Normal file
28
backend/services/controller/internal/db/db.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
devices *mongo.Collection
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewDatabase(ctx context.Context, mongoUri string) Database {
|
||||
var db Database
|
||||
clientOptions := options.Client().ApplyURI(mongoUri)
|
||||
client, err := mongo.Connect(ctx, clientOptions)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Connected to MongoDB-->", mongoUri)
|
||||
devices := client.Database("oktopus").Collection("devices")
|
||||
db.devices = devices
|
||||
db.ctx = ctx
|
||||
return db
|
||||
}
|
||||
50
backend/services/controller/internal/db/device.go
Normal file
50
backend/services/controller/internal/db/device.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
SN string
|
||||
Model string
|
||||
Customer string
|
||||
Vendor string
|
||||
Version string
|
||||
}
|
||||
|
||||
func (d *Database) CreateDevice(device Device) error {
|
||||
var result bson.M
|
||||
opts := options.FindOneAndReplace().SetUpsert(true)
|
||||
err := d.devices.FindOneAndReplace(d.ctx, bson.D{{"sn", device.SN}}, device, opts).Decode(&result)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
log.Printf("New device %s added to database", device.SN)
|
||||
return nil
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Device %s already existed, and got replaced for new info", device.SN)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) RetrieveDevices() ([]Device, error) {
|
||||
var results []Device
|
||||
//TODO: filter devices by user ownership
|
||||
cursor, err := d.devices.Find(d.ctx, bson.D{}, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
if err = cursor.All(d.ctx, &results); err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (d *Database) DeleteDevice() {
|
||||
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
package mqtt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/eclipse/paho.golang/paho"
|
||||
"github.com/leandrofars/oktopus/internal/utils"
|
||||
)
|
||||
|
||||
type Mqtt struct {
|
||||
Addr string
|
||||
Port string
|
||||
Id string
|
||||
User string
|
||||
Passwd string
|
||||
Ctx context.Context
|
||||
QoS int
|
||||
SubTopic string
|
||||
CA string
|
||||
}
|
||||
|
||||
var c *paho.Client
|
||||
|
||||
/* ------------------- Implementations of broker interface ------------------ */
|
||||
|
||||
func (m *Mqtt) Connect() {
|
||||
msgChan := make(chan *paho.Publish)
|
||||
go messageHandler(msgChan)
|
||||
clientConfig := startClient(m.Addr, m.Port, m.CA, m.Ctx, msgChan)
|
||||
connParameters := startConnection(m.Id, m.User, m.Passwd)
|
||||
|
||||
conn, err := clientConfig.Connect(m.Ctx, &connParameters)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
// Sets global client to be used by other mqtt functions
|
||||
c = clientConfig
|
||||
|
||||
if conn.ReasonCode != 0 {
|
||||
log.Fatalf("Failed to connect to %s : %d - %s", m.Addr, conn.ReasonCode, conn.Properties.ReasonString)
|
||||
}
|
||||
|
||||
log.Printf("Connected to broker--> %s:%s", m.Addr, m.Port)
|
||||
}
|
||||
|
||||
func (m *Mqtt) Disconnect() {
|
||||
d := &paho.Disconnect{ReasonCode: 0}
|
||||
err := c.Disconnect(d)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to send Disconnect: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mqtt) Subscribe() {
|
||||
if _, err := c.Subscribe(m.Ctx, &paho.Subscribe{
|
||||
Subscriptions: map[string]paho.SubscribeOptions{
|
||||
m.SubTopic: {QoS: byte(m.QoS), NoLocal: true},
|
||||
},
|
||||
}); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
log.Printf("Subscribed to %s", m.SubTopic)
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
func startClient(addr string, port string, tlsCa string, ctx context.Context, msgChan chan *paho.Publish) *paho.Client {
|
||||
singleHandler := paho.NewSingleHandlerRouter(func(m *paho.Publish) {
|
||||
msgChan <- m
|
||||
})
|
||||
|
||||
if tlsCa != "" {
|
||||
conn := connWithTls(tlsCa, addr+":"+port, ctx)
|
||||
clientConfig := paho.ClientConfig{
|
||||
Conn: conn,
|
||||
Router: singleHandler,
|
||||
OnServerDisconnect: func(disconnect *paho.Disconnect) {
|
||||
log.Println("disconnected from mqtt server, reason code: ", disconnect.ReasonCode)
|
||||
},
|
||||
OnClientError: func(err error) {
|
||||
log.Println(err)
|
||||
},
|
||||
}
|
||||
return paho.NewClient(clientConfig)
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", addr+":"+port)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
clientConfig := paho.ClientConfig{
|
||||
Conn: conn,
|
||||
Router: singleHandler,
|
||||
}
|
||||
|
||||
return paho.NewClient(clientConfig)
|
||||
}
|
||||
|
||||
func connWithTls(tlsCa, address string, ctx context.Context) net.Conn {
|
||||
ca, err := ioutil.ReadFile(tlsCa)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
ok := roots.AppendCertsFromPEM(ca)
|
||||
if !ok {
|
||||
panic("failed to parse root certificate")
|
||||
}
|
||||
|
||||
config := &tls.Config{
|
||||
// After going to cloud, certificates must match names, and we must take this option below
|
||||
InsecureSkipVerify: true,
|
||||
RootCAs: roots,
|
||||
}
|
||||
|
||||
d := tls.Dialer{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
conn, err := d.DialContext(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
conn = newThreadSafeConnection(conn)
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// Custom net.Conn with thread safety
|
||||
func newThreadSafeConnection(c net.Conn) net.Conn {
|
||||
type threadSafeConn struct {
|
||||
net.Conn
|
||||
sync.Locker
|
||||
}
|
||||
|
||||
return &threadSafeConn{
|
||||
Conn: c,
|
||||
Locker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func startConnection(id, user, pass string) paho.Connect {
|
||||
|
||||
connParameters := paho.Connect{
|
||||
KeepAlive: 30,
|
||||
ClientID: id,
|
||||
CleanStart: true,
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
connParameters.ClientID = id
|
||||
} else {
|
||||
mac, err := utils.GetMacAddr()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
connParameters.ClientID = mac[0]
|
||||
}
|
||||
|
||||
if user != "" {
|
||||
connParameters.Username = user
|
||||
connParameters.UsernameFlag = true
|
||||
}
|
||||
if pass != "" {
|
||||
connParameters.Password = []byte(pass)
|
||||
connParameters.PasswordFlag = true
|
||||
}
|
||||
|
||||
return connParameters
|
||||
}
|
||||
|
||||
func messageHandler(msg chan *paho.Publish) {
|
||||
for m := range msg {
|
||||
log.Println("Received message:", string(m.Payload))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
Runs MQTT broker trough a Docker container.
|
||||
Better approach would be to use docker api to Go language, but os/exec lib is already enough for our purpose,
|
||||
since it's more convenient and easier to use docker shell commands, and it's already a start point.
|
||||
*/
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Get Mqtt Broker up and running
|
||||
func StartMqttBroker() {
|
||||
|
||||
//TODO: Start Container through Docker SDK for GO, eliminating docker-compose and shell comands.
|
||||
//TODO: Create Broker with user, password and CA certificate.
|
||||
//TODO: Set MQTTv5 CONNACK packet with topic for agent to use.
|
||||
|
||||
cmd := exec.Command("sudo", "docker", "compose", "-f", "internal/mosquitto/docker-compose.yml", "up", "-d")
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Broker Mqtt Up and Running!")
|
||||
}
|
||||
295
backend/services/controller/internal/mqtt/mqtt.go
Normal file
295
backend/services/controller/internal/mqtt/mqtt.go
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
package mqtt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"github.com/eclipse/paho.golang/paho"
|
||||
"github.com/leandrofars/oktopus/internal/db"
|
||||
usp_msg "github.com/leandrofars/oktopus/internal/usp_message"
|
||||
"github.com/leandrofars/oktopus/internal/usp_record"
|
||||
"github.com/leandrofars/oktopus/internal/utils"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Mqtt struct {
|
||||
Addr string
|
||||
Port string
|
||||
Id string
|
||||
User string
|
||||
Passwd string
|
||||
Ctx context.Context
|
||||
QoS int
|
||||
SubTopic string
|
||||
DevicesTopic string
|
||||
CA string
|
||||
DB db.Database
|
||||
}
|
||||
|
||||
var c *paho.Client
|
||||
|
||||
/* ------------------- Implementations of broker interface ------------------ */
|
||||
|
||||
func (m *Mqtt) Connect() {
|
||||
devices := make(chan *paho.Publish)
|
||||
controller := make(chan *paho.Publish)
|
||||
go m.messageHandler(devices, controller)
|
||||
clientConfig := m.startClient(devices, controller)
|
||||
connParameters := startConnection(m.Id, m.User, m.Passwd)
|
||||
|
||||
conn, err := clientConfig.Connect(m.Ctx, &connParameters)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if conn.ReasonCode != 0 {
|
||||
log.Fatalf("Failed to connect to %s : %d - %s", m.Addr+m.Port, conn.ReasonCode, conn.Properties.ReasonString)
|
||||
}
|
||||
|
||||
// Sets global client to be used by other mqtt functions
|
||||
c = clientConfig
|
||||
|
||||
log.Printf("Connected to broker--> %s:%s", m.Addr, m.Port)
|
||||
}
|
||||
|
||||
func (m *Mqtt) Disconnect() {
|
||||
d := &paho.Disconnect{ReasonCode: 0}
|
||||
err := c.Disconnect(d)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to send Disconnect: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mqtt) Subscribe() {
|
||||
if _, err := c.Subscribe(m.Ctx, &paho.Subscribe{
|
||||
Subscriptions: map[string]paho.SubscribeOptions{
|
||||
m.SubTopic: {QoS: byte(m.QoS), NoLocal: true},
|
||||
m.DevicesTopic: {QoS: byte(m.QoS), NoLocal: true},
|
||||
},
|
||||
}); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
log.Printf("Subscribed to %s", m.SubTopic)
|
||||
log.Printf("Subscribed to %s", m.DevicesTopic)
|
||||
}
|
||||
|
||||
func (m *Mqtt) Publish(msg []byte, topic, respTopic string) {
|
||||
if _, err := c.Publish(context.Background(), &paho.Publish{
|
||||
Topic: topic,
|
||||
QoS: byte(m.QoS),
|
||||
Retain: false,
|
||||
Payload: msg,
|
||||
Properties: &paho.PublishProperties{
|
||||
ResponseTopic: respTopic,
|
||||
},
|
||||
}); err != nil {
|
||||
log.Println("error sending message:", err)
|
||||
}
|
||||
|
||||
log.Printf("Published to %s", topic)
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
func (m *Mqtt) startClient(devices, controller chan *paho.Publish) *paho.Client {
|
||||
singleHandler := paho.NewSingleHandlerRouter(func(p *paho.Publish) {
|
||||
if p.Topic == m.DevicesTopic {
|
||||
devices <- p
|
||||
} else if strings.Contains(p.Topic, "controller") {
|
||||
controller <- p
|
||||
} else {
|
||||
log.Println("No handler for topic: ", p.Topic)
|
||||
}
|
||||
})
|
||||
|
||||
if m.CA != "" {
|
||||
conn := connWithTls(m.CA, m.Addr+":"+m.Port, m.Ctx)
|
||||
clientConfig := paho.ClientConfig{
|
||||
Conn: conn,
|
||||
Router: singleHandler,
|
||||
OnServerDisconnect: func(disconnect *paho.Disconnect) {
|
||||
log.Println("disconnected from mqtt server, reason code: ", disconnect.ReasonCode)
|
||||
},
|
||||
OnClientError: func(err error) {
|
||||
log.Println(err)
|
||||
},
|
||||
}
|
||||
return paho.NewClient(clientConfig)
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", m.Addr+":"+m.Port)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
clientConfig := paho.ClientConfig{
|
||||
Conn: conn,
|
||||
Router: singleHandler,
|
||||
}
|
||||
|
||||
return paho.NewClient(clientConfig)
|
||||
}
|
||||
|
||||
func connWithTls(tlsCa, address string, ctx context.Context) net.Conn {
|
||||
ca, err := ioutil.ReadFile(tlsCa)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
ok := roots.AppendCertsFromPEM(ca)
|
||||
if !ok {
|
||||
panic("failed to parse root certificate")
|
||||
}
|
||||
|
||||
config := &tls.Config{
|
||||
// After going to cloud, certificates must match names, and we must take this option below
|
||||
InsecureSkipVerify: true,
|
||||
RootCAs: roots,
|
||||
}
|
||||
|
||||
d := tls.Dialer{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
conn, err := d.DialContext(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
conn = newThreadSafeConnection(conn)
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// Custom net.Conn with thread safety
|
||||
func newThreadSafeConnection(c net.Conn) net.Conn {
|
||||
type threadSafeConn struct {
|
||||
net.Conn
|
||||
sync.Locker
|
||||
}
|
||||
|
||||
return &threadSafeConn{
|
||||
Conn: c,
|
||||
Locker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func startConnection(id, user, pass string) paho.Connect {
|
||||
|
||||
connParameters := paho.Connect{
|
||||
KeepAlive: 30,
|
||||
ClientID: id,
|
||||
CleanStart: true,
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
connParameters.ClientID = id
|
||||
} else {
|
||||
mac, err := utils.GetMacAddr()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
connParameters.ClientID = mac[0]
|
||||
}
|
||||
|
||||
if user != "" {
|
||||
connParameters.Username = user
|
||||
connParameters.UsernameFlag = true
|
||||
}
|
||||
if pass != "" {
|
||||
connParameters.Password = []byte(pass)
|
||||
connParameters.PasswordFlag = true
|
||||
}
|
||||
|
||||
return connParameters
|
||||
}
|
||||
|
||||
func (m *Mqtt) messageHandler(devices, controller chan *paho.Publish) {
|
||||
for {
|
||||
select {
|
||||
case d := <-devices:
|
||||
payload := string(d.Payload)
|
||||
log.Println("New device: ", payload)
|
||||
m.handleNewDevice(payload)
|
||||
case c := <-controller:
|
||||
m.handleDevicesResponse(c.Payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mqtt) handleNewDevice(deviceMac string) {
|
||||
payload := usp_msg.Msg{
|
||||
Header: &usp_msg.Header{
|
||||
MsgId: "uniqueIdentifierForThismessage",
|
||||
MsgType: usp_msg.Header_GET,
|
||||
},
|
||||
Body: &usp_msg.Body{
|
||||
MsgBody: &usp_msg.Body_Request{
|
||||
Request: &usp_msg.Request{
|
||||
ReqType: &usp_msg.Request_Get{
|
||||
Get: &usp_msg.Get{
|
||||
ParamPaths: []string{
|
||||
"Device.DeviceInfo.Manufacturer",
|
||||
"Device.DeviceInfo.ModelName",
|
||||
"Device.DeviceInfo.SoftwareVersion",
|
||||
"Device.DeviceInfo.SerialNumber",
|
||||
},
|
||||
MaxDepth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
teste, _ := proto.Marshal(&payload)
|
||||
record := usp_record.Record{
|
||||
Version: "0.1",
|
||||
ToId: deviceMac,
|
||||
FromId: "leleco",
|
||||
PayloadSecurity: usp_record.Record_PLAINTEXT,
|
||||
RecordType: &usp_record.Record_NoSessionContext{
|
||||
NoSessionContext: &usp_record.NoSessionContextRecord{
|
||||
Payload: teste,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tr369Message, err := proto.Marshal(&record)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to encode address book:", err)
|
||||
}
|
||||
m.Publish(tr369Message, "oktopus/v1/agent/"+deviceMac, "oktopus/v1/controller/"+deviceMac)
|
||||
}
|
||||
|
||||
func (m *Mqtt) handleDevicesResponse(p []byte) {
|
||||
var record usp_record.Record
|
||||
var message usp_msg.Msg
|
||||
|
||||
err := proto.Unmarshal(p, &record)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = proto.Unmarshal(record.GetNoSessionContext().Payload, &message)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var device db.Device
|
||||
msg := message.Body.MsgBody.(*usp_msg.Body_Response).Response.GetGetResp()
|
||||
|
||||
device.Vendor = msg.ReqPathResults[0].ResolvedPathResults[0].ResultParams["Manufacturer"]
|
||||
device.Model = msg.ReqPathResults[1].ResolvedPathResults[0].ResultParams["ModelName"]
|
||||
device.Version = msg.ReqPathResults[2].ResolvedPathResults[0].ResultParams["SoftwareVersion"]
|
||||
device.SN = msg.ReqPathResults[3].ResolvedPathResults[0].ResultParams["SerialNumber"]
|
||||
|
||||
err = m.DB.CreateDevice(device)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import (
|
|||
type Broker interface {
|
||||
Connect()
|
||||
Disconnect()
|
||||
// Publish()
|
||||
Publish(msg []byte, topic, respTopic string)
|
||||
Subscribe()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,404 +1,3 @@
|
|||
This broker is an implementation of mochi. I had to fork it to customize CONNACK packet userProperty.
|
||||
|
||||
<p align="center">
|
||||
|
||||

|
||||
[](https://coveralls.io/github/mochi-co/mqtt?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/mochi-co/mqtt/v2)
|
||||
[](https://pkg.go.dev/github.com/mochi-co/mqtt/v2)
|
||||
[](https://github.com/mochi-co/mqtt/issues)
|
||||
|
||||
</p>
|
||||
|
||||
# Mochi MQTT Broker
|
||||
## The fully compliant, embeddable high-performance Go MQTT v5 (and v3.1.1) broker server
|
||||
Mochi MQTT is an embeddable [fully compliant](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html) MQTT v5 broker server written in Go, designed for the development of telemetry and internet-of-things projects. The server can be used either as a standalone binary or embedded as a library in your own applications, and has been designed to be as lightweight and fast as possible, with great care taken to ensure the quality and maintainability of the project.
|
||||
|
||||
### What is MQTT?
|
||||
MQTT stands for [MQ Telemetry Transport](https://en.wikipedia.org/wiki/MQTT). It is a publish/subscribe, extremely simple and lightweight messaging protocol, designed for constrained devices and low-bandwidth, high-latency or unreliable networks ([Learn more](https://mqtt.org/faq)). Mochi MQTT fully implements version 5.0.0 of the MQTT protocol.
|
||||
|
||||
## What's new in Version 2.0.0?
|
||||
Version 2.0.0 takes all the great things we loved about Mochi MQTT v1.0.0, learns from the mistakes, and improves on the things we wished we'd had. It's a total from-scratch rewrite, designed to fully implement MQTT v5 as a first-class feature.
|
||||
|
||||
Don't forget to use the new v2 import paths:
|
||||
```go
|
||||
import "github.com/mochi-co/mqtt/v2"
|
||||
```
|
||||
|
||||
- Full MQTTv5 Feature Compliance, compatibility for MQTT v3.1.1 and v3.0.0:
|
||||
- User and MQTTv5 Packet Properties
|
||||
- Topic Aliases
|
||||
- Shared Subscriptions
|
||||
- Subscription Options and Subscription Identifiers
|
||||
- Message Expiry
|
||||
- Client Session Expiry
|
||||
- Send and Receive QoS Flow Control Quotas
|
||||
- Server-side Disconnect and Auth Packets
|
||||
- Will Delay Intervals
|
||||
- Plus all the original MQTT features of Mochi MQTT v1, such as Full QoS(0,1,2), $SYS topics, retained messages, etc.
|
||||
- Developer-centric:
|
||||
- Most core broker code is now exported and accessible, for total developer control.
|
||||
- Full featured and flexible Hook-based interfacing system to provide easy 'plugin' development.
|
||||
- Direct Packet Injection using special inline client, or masquerade as existing clients.
|
||||
- Performant and Stable:
|
||||
- Our classic trie-based Topic-Subscription model.
|
||||
- Client-specific write buffers to avoid issues with slow-reading or irregular client behaviour.
|
||||
- Passes all [Paho Interoperability Tests](https://github.com/eclipse/paho.mqtt.testing/tree/master/interoperability) for MQTT v5 and MQTT v3.
|
||||
- Over a thousand carefully considered unit test scenarios.
|
||||
- TCP, Websocket (including SSL/TLS), and $SYS Dashboard listeners.
|
||||
- Built-in Redis, Badger, and Bolt Persistence using Hooks (but you can also make your own).
|
||||
- Built-in Rule-based Authentication and ACL Ledger using Hooks (also make your own).
|
||||
|
||||
> There is no upgrade path from v1.0.0. Please review the documentation and this readme to get a sense of the changes required (e.g. the v1 events system, auth, and persistence have all been replaced with the new hooks system).
|
||||
|
||||
### Compatibility Notes
|
||||
Because of the overlap between the v5 specification and previous versions of mqtt, the server can accept both v5 and v3 clients, but note that in cases where both v5 an v3 clients are connected, properties and features provided for v5 clients will be downgraded for v3 clients (such as user properties).
|
||||
|
||||
Support for MQTT v3.0.0 and v3.1.1 is considered hybrid-compatibility. Where not specifically restricted in the v3 specification, more modern and safety-first v5 behaviours are used instead - such as expiry for inflight and retained messages, and clients - and quality-of-service flow control limits.
|
||||
|
||||
## Roadmap
|
||||
- Please [open an issue](https://github.com/mochi-co/mqtt/issues) to request new features or event hooks!
|
||||
- Cluster support.
|
||||
- Enhanced Metrics support.
|
||||
- File-based server configuration (supporting docker).
|
||||
|
||||
## Quick Start
|
||||
### Running the Broker with Go
|
||||
Mochi MQTT can be used as a standalone broker. Simply checkout this repository and run the [cmd/main.go](cmd/main.go) entrypoint in the [cmd](cmd) folder which will expose tcp (:1883), websocket (:1882), and dashboard (:8080) listeners.
|
||||
|
||||
```
|
||||
cd cmd
|
||||
go build -o mqtt && ./mqtt
|
||||
```
|
||||
|
||||
### Using Docker
|
||||
A simple Dockerfile is provided for running the [cmd/main.go](cmd/main.go) Websocket, TCP, and Stats server:
|
||||
|
||||
```sh
|
||||
docker build -t mochi:latest .
|
||||
docker run -p 1883:1883 -p 1882:1882 -p 8080:8080 mochi:latest
|
||||
```
|
||||
|
||||
## Developing with Mochi MQTT
|
||||
### Importing as a package
|
||||
Importing Mochi MQTT as a package requires just a few lines of code to get started.
|
||||
``` go
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/mochi-co/mqtt/v2"
|
||||
"github.com/mochi-co/mqtt/v2/hooks/auth"
|
||||
"github.com/mochi-co/mqtt/v2/listeners"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create the new MQTT Server.
|
||||
server := mqtt.New(nil)
|
||||
|
||||
// Allow all connections.
|
||||
_ = server.AddHook(new(auth.AllowHook), nil)
|
||||
|
||||
// Create a TCP listener on a standard port.
|
||||
tcp := listeners.NewTCP("t1", ":1883", nil)
|
||||
err := server.AddListener(tcp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = server.Serve()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Examples of running the broker with various configurations can be found in the [examples](examples) folder.
|
||||
|
||||
#### Network Listeners
|
||||
The server comes with a variety of pre-packaged network listeners which allow the broker to accept connections on different protocols. The current listeners are:
|
||||
|
||||
| Listener | Usage |
|
||||
| --- | --- |
|
||||
| listeners.NewTCP | A TCP listener |
|
||||
| listeners.NewUnixSock | A Unix Socket listener |
|
||||
| listeners.NewNet | A net.Listener listener |
|
||||
| listeners.NewWebsocket | A Websocket listener |
|
||||
| listeners.NewHTTPStats | An HTTP $SYS info dashboard |
|
||||
|
||||
> Use the `listeners.Listener` interface to develop new listeners. If you do, please let us know!
|
||||
|
||||
A `*listeners.Config` may be passed to configure TLS.
|
||||
|
||||
Examples of usage can be found in the [examples](examples) folder or [cmd/main.go](cmd/main.go).
|
||||
|
||||
### Server Options and Capabilities
|
||||
A number of configurable options are available which can be used to alter the behaviour or restrict access to certain features in the server.
|
||||
|
||||
```go
|
||||
server := mqtt.New(&mqtt.Options{
|
||||
Capabilities: mqtt.Capabilities{
|
||||
ClientNetWriteBufferSize: 4096,
|
||||
ClientNetReadBufferSize: 4096,
|
||||
MaximumSessionExpiryInterval: 3600,
|
||||
Compatibilities: mqtt.Compatibilities{
|
||||
ObscureNotAuthorized: true,
|
||||
},
|
||||
},
|
||||
SysTopicResendInterval: 10,
|
||||
})
|
||||
```
|
||||
|
||||
Review the mqtt.Options, mqtt.Capabilities, and mqtt.Compatibilities structs for a comprehensive list of options. `ClientNetWriteBufferSize` and `ClientNetReadBufferSize` can be configured to adjust memory usage per client, based on your needs.
|
||||
|
||||
|
||||
## Event Hooks
|
||||
A universal event hooks system allows developers to hook into various parts of the server and client life cycle to add and modify functionality of the broker. These universal hooks are used to provide everything from authentication, persistent storage, to debugging tools.
|
||||
|
||||
Hooks are stackable - you can add multiple hooks to a server, and they will be run in the order they were added. Some hooks modify values, and these modified values will be passed to the subsequent hooks before being returned to the runtime code.
|
||||
|
||||
| Type | Import | Info |
|
||||
| -- | -- | -- |
|
||||
| Access Control | [mochi-co/mqtt/hooks/auth . AllowHook](hooks/auth/allow_all.go) | Allow access to all connecting clients and read/write to all topics. |
|
||||
| Access Control | [mochi-co/mqtt/hooks/auth . Auth](hooks/auth/auth.go) | Rule-based access control ledger. |
|
||||
| Persistence | [mochi-co/mqtt/hooks/storage/bolt](hooks/storage/bolt/bolt.go) | Persistent storage using [BoltDB](https://dbdb.io/db/boltdb) (deprecated). |
|
||||
| Persistence | [mochi-co/mqtt/hooks/storage/badger](hooks/storage/badger/badger.go) | Persistent storage using [BadgerDB](https://github.com/dgraph-io/badger). |
|
||||
| Persistence | [mochi-co/mqtt/hooks/storage/redis](hooks/storage/redis/redis.go) | Persistent storage using [Redis](https://redis.io). |
|
||||
| Debugging | [mochi-co/mqtt/hooks/debug](hooks/debug/debug.go) | Additional debugging output to visualise packet flow. |
|
||||
|
||||
Many of the internal server functions are now exposed to developers, so you can make your own Hooks by using the above as examples. If you do, please [Open an issue](https://github.com/mochi-co/mqtt/issues) and let everyone know!
|
||||
|
||||
### Access Control
|
||||
#### Allow Hook
|
||||
By default, Mochi MQTT uses a DENY-ALL access control rule. To allow connections, this must overwritten using an Access Control hook. The simplest of these hooks is the `auth.AllowAll` hook, which provides ALLOW-ALL rules to all connections, subscriptions, and publishing. It's also the simplest hook to use:
|
||||
|
||||
```go
|
||||
server := mqtt.New(nil)
|
||||
_ = server.AddHook(new(auth.AllowHook), nil)
|
||||
```
|
||||
|
||||
> Don't do this if you are exposing your server to the internet or untrusted networks - it should really be used for development, testing, and debugging only.
|
||||
|
||||
#### Auth Ledger
|
||||
The Auth Ledger hook provides a sophisticated mechanism for defining access rules in a struct format. Auth ledger rules come in two forms: Auth rules (connection), and ACL rules (publish subscribe).
|
||||
|
||||
Auth rules have 4 optional criteria and an assertion flag:
|
||||
| Criteria | Usage |
|
||||
| -- | -- |
|
||||
| Client | client id of the connecting client |
|
||||
| Username | username of the connecting client |
|
||||
| Password | password of the connecting client |
|
||||
| Remote | the remote address or ip of the client |
|
||||
| Allow | true (allow this user) or false (deny this user) |
|
||||
|
||||
ACL rules have 3 optional criteria and an filter match:
|
||||
| Criteria | Usage |
|
||||
| -- | -- |
|
||||
| Client | client id of the connecting client |
|
||||
| Username | username of the connecting client |
|
||||
| Remote | the remote address or ip of the client |
|
||||
| Filters | an array of filters to match |
|
||||
|
||||
Rules are processed in index order (0,1,2,3), returning on the first matching rule. See [hooks/auth/ledger.go](hooks/auth/ledger.go) to review the structs.
|
||||
|
||||
```go
|
||||
server := mqtt.New(nil)
|
||||
err := server.AddHook(new(auth.Hook), &auth.Options{
|
||||
Ledger: &auth.Ledger{
|
||||
Auth: auth.AuthRules{ // Auth disallows all by default
|
||||
{Username: "peach", Password: "password1", Allow: true},
|
||||
{Username: "melon", Password: "password2", Allow: true},
|
||||
{Remote: "127.0.0.1:*", Allow: true},
|
||||
{Remote: "localhost:*", Allow: true},
|
||||
},
|
||||
ACL: auth.ACLRules{ // ACL allows all by default
|
||||
{Remote: "127.0.0.1:*"}, // local superuser allow all
|
||||
{
|
||||
// user melon can read and write to their own topic
|
||||
Username: "melon", Filters: auth.Filters{
|
||||
"melon/#": auth.ReadWrite,
|
||||
"updates/#": auth.WriteOnly, // can write to updates, but can't read updates from others
|
||||
},
|
||||
},
|
||||
{
|
||||
// Otherwise, no clients have publishing permissions
|
||||
Filters: auth.Filters{
|
||||
"#": auth.ReadOnly,
|
||||
"updates/#": auth.Deny,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
The ledger can also be stored as JSON or YAML and loaded using the Data field:
|
||||
```go
|
||||
err = server.AddHook(new(auth.Hook), &auth.Options{
|
||||
Data: data, // build ledger from byte slice: yaml or json
|
||||
})
|
||||
```
|
||||
See [examples/auth/encoded/main.go](examples/auth/encoded/main.go) for more information.
|
||||
|
||||
### Persistent Storage
|
||||
#### Redis
|
||||
A basic Redis storage hook is available which provides persistence for the broker. It can be added to the server in the same fashion as any other hook, with several options. It uses github.com/go-redis/redis/v8 under the hook, and is completely configurable through the Options value.
|
||||
```go
|
||||
err := server.AddHook(new(redis.Hook), &redis.Options{
|
||||
Options: &rv8.Options{
|
||||
Addr: "localhost:6379", // default redis address
|
||||
Password: "", // your password
|
||||
DB: 0, // your redis db
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
For more information on how the redis hook works, or how to use it, see the [examples/persistence/redis/main.go](examples/persistence/redis/main.go) or [hooks/storage/redis](hooks/storage/redis) code.
|
||||
|
||||
#### Badger DB
|
||||
There's also a BadgerDB storage hook if you prefer file based storage. It can be added and configured in much the same way as the other hooks (with somewhat less options).
|
||||
```go
|
||||
err := server.AddHook(new(badger.Hook), &badger.Options{
|
||||
Path: badgerPath,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
For more information on how the badger hook works, or how to use it, see the [examples/persistence/badger/main.go](examples/persistence/badger/main.go) or [hooks/storage/badger](hooks/storage/badger) code.
|
||||
|
||||
There is also a BoltDB hook which has been deprecated in favour of Badger, but if you need it, check [examples/persistence/bolt/main.go](examples/persistence/bolt/main.go).
|
||||
|
||||
|
||||
|
||||
## Developing with Event Hooks
|
||||
Many hooks are available for interacting with the broker and client lifecycle.
|
||||
The function signatures for all the hooks and `mqtt.Hook` interface can be found in [hooks.go](hooks.go).
|
||||
|
||||
> The most flexible event hooks are OnPacketRead, OnPacketEncode, and OnPacketSent - these hooks be used to control and modify all incoming and outgoing packets.
|
||||
|
||||
| Function | Usage |
|
||||
| -------------------------- | -- |
|
||||
| OnStarted | Called when the server has successfully started.|
|
||||
| OnStopped | Called when the server has successfully stopped. |
|
||||
| OnConnectAuthenticate | Called when a user attempts to authenticate with the server. An implementation of this method MUST be used to allow or deny access to the server (see hooks/auth/allow_all or basic). It can be used in custom hooks to check connecting users against an existing user database. Returns true if allowed. |
|
||||
| OnACLCheck | Called when a user attempts to publish or subscribe to a topic filter. As above. |
|
||||
| OnSysInfoTick | Called when the $SYS topic values are published out. |
|
||||
| OnConnect | Called when a new client connects |
|
||||
| OnSessionEstablished | Called when a new client successfully establishes a session (after OnConnect) |
|
||||
| OnDisconnect | Called when a client is disconnected for any reason. |
|
||||
| OnAuthPacket | Called when an auth packet is received. It is intended to allow developers to create their own mqtt v5 Auth Packet handling mechanisms. Allows packet modification. |
|
||||
| OnPacketRead | Called when a packet is received from a client. Allows packet modification. |
|
||||
| OnPacketEncode | Called immediately before a packet is encoded to be sent to a client. Allows packet modification. |
|
||||
| OnPacketSent | Called when a packet has been sent to a client. |
|
||||
| OnPacketProcessed | Called when a packet has been received and successfully handled by the broker. |
|
||||
| OnSubscribe | Called when a client subscribes to one or more filters. Allows packet modification. |
|
||||
| OnSubscribed | Called when a client successfully subscribes to one or more filters. |
|
||||
| OnSelectSubscribers | Called when subscribers have been collected for a topic, but before shared subscription subscribers have been selected. Allows receipient modification.|
|
||||
| OnUnsubscribe | Called when a client unsubscribes from one or more filters. Allows packet modification. |
|
||||
| OnUnsubscribed | Called when a client successfully unsubscribes from one or more filters. |
|
||||
| OnPublish | Called when a client publishes a message. Allows packet modification. |
|
||||
| OnPublished | Called when a client has published a message to subscribers. |
|
||||
| OnPublishDropped | Called when a message to a client is dropped before delivery, such as if the client is taking too long to respond. |
|
||||
| OnRetainMessage | Called then a published message is retained. |
|
||||
| OnQosPublish | Called when a publish packet with Qos >= 1 is issued to a subscriber. |
|
||||
| OnQosComplete | Called when the Qos flow for a message has been completed. |
|
||||
| OnQosDropped | Called when an inflight message expires before completion. |
|
||||
| OnWill | Called when a client disconnects and intends to issue a will message. Allows packet modification. |
|
||||
| OnWillSent | Called when an LWT message has been issued from a disconnecting client. |
|
||||
| OnClientExpired | Called when a client session has expired and should be deleted. |
|
||||
| OnRetainedExpired | Called when a retained message has expired and should be deleted. |
|
||||
| StoredClients | Returns clients, eg. from a persistent store. |
|
||||
| StoredSubscriptions | Returns client subscriptions, eg. from a persistent store. |
|
||||
| StoredInflightMessages | Returns inflight messages, eg. from a persistent store. |
|
||||
| StoredRetainedMessages | Returns retained messages, eg. from a persistent store. |
|
||||
| StoredSysInfo | Returns stored system info values, eg. from a persistent store. |
|
||||
|
||||
If you are building a persistent storage hook, see the existing persistent hooks for inspiration and patterns. If you are building an auth hook, you will need `OnACLCheck` and `OnConnectAuthenticate`.
|
||||
|
||||
|
||||
### Direct Publish
|
||||
To publish basic message to a topic from within the embedding application, you can use the `server.Publish(topic string, payload []byte, retain bool, qos byte) error` method.
|
||||
|
||||
```go
|
||||
err := server.Publish("direct/publish", []byte("packet scheduled message"), false, 0)
|
||||
```
|
||||
> The Qos byte in this case is only used to set the upper qos limit available for subscribers, as per MQTT v5 spec.
|
||||
|
||||
### Packet Injection
|
||||
If you want more control, or want to set specific MQTT v5 properties and other values you can create your own publish packets from a client of your choice. This method allows you to inject MQTT packets (no just publish) directly into the runtime as though they had been received by a specific client. Most of the time you'll want to use the special client flag `inline=true`, as it has unique privileges: it bypasses all ACL and topic validation checks, meaning it can even publish to $SYS topics.
|
||||
|
||||
Packet injection can be used for any MQTT packet, including ping requests, subscriptions, etc. And because the Clients structs and methods are now exported, you can even inject packets on behalf of a connected client (if you have a very custom requirements).
|
||||
|
||||
```go
|
||||
cl := server.NewClient(nil, "local", "inline", true)
|
||||
server.InjectPacket(cl, packets.Packet{
|
||||
FixedHeader: packets.FixedHeader{
|
||||
Type: packets.Publish,
|
||||
},
|
||||
TopicName: "direct/publish",
|
||||
Payload: []byte("scheduled message"),
|
||||
})
|
||||
```
|
||||
|
||||
> MQTT packets still need to be correctly formed, so refer our [the test packets catalogue](packets/tpackets.go) and [MQTTv5 Specification](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html) for inspiration.
|
||||
|
||||
See the [hooks example](examples/hooks/main.go) to see this feature in action.
|
||||
|
||||
|
||||
|
||||
### Testing
|
||||
#### Unit Tests
|
||||
Mochi MQTT tests over a thousand scenarios with thoughtfully hand written unit tests to ensure each function does exactly what we expect. You can run the tests using go:
|
||||
```
|
||||
go run --cover ./...
|
||||
```
|
||||
|
||||
#### Paho Interoperability Test
|
||||
You can check the broker against the [Paho Interoperability Test](https://github.com/eclipse/paho.mqtt.testing/tree/master/interoperability) by starting the broker using `examples/paho/main.go`, and then running the mqtt v5 and v3 tests with `python3 client_test5.py` from the _interoperability_ folder.
|
||||
|
||||
> Note that there are currently a number of outstanding issues regarding false negatives in the paho suite, and as such, certain compatibility modes are enabled in the `paho/main.go` example.
|
||||
|
||||
|
||||
## Performance Benchmarks
|
||||
Mochi MQTT performance is comparable with popular brokers such as Mosquitto, EMQX, and others.
|
||||
|
||||
Performance benchmarks were tested using [MQTT-Stresser](https://github.com/inovex/mqtt-stresser) on a Apple Macbook Air M2, using `cmd/main.go` default settings. Taking into account bursts of high and low throughput, the median scores are the most useful. Higher is better.
|
||||
|
||||
> The values presented in the benchmark are not representative of true messages per second throughput. They rely on an unusual calculation by mqtt-stresser, but are usable as they are consistent across all brokers.
|
||||
> Benchmarks are provided as a general performance expectation guideline only.
|
||||
|
||||
`mqtt-stresser -broker tcp://localhost:1883 -num-clients=2 -num-messages=10000`
|
||||
| Broker | publish fastest | median | slowest | receive fastest | median | slowest |
|
||||
| -- | -- | -- | -- | -- | -- | -- |
|
||||
| Mochi v2.2.0 | 127,216 | 125,748 | 124,279 | 319,250 | 309,327 | 299,405 |
|
||||
| Mosquitto v2.0.15 | 155,920 | 155,919 | 155,918 | 185,485 | 185,097 | 184,709 |
|
||||
| EMQX v5.0.11 | 156,945 | 156,257 | 155,568 | 17,918 | 17,783 | 17,649 |
|
||||
|
||||
`mqtt-stresser -broker tcp://localhost:1883 -num-clients=10 -num-messages=10000`
|
||||
| Broker | publish fastest | median | slowest | receive fastest | median | slowest |
|
||||
| -- | -- | -- | -- | -- | -- | -- |
|
||||
| Mochi v2.2.0 | 45,615 | 30,129 | 21,138 | 232,717 | 86,323 | 50,402 |
|
||||
| Mosquitto v2.0.15 | 42,729 | 38,633 | 29,879 | 23,241 | 19,714 | 18,806 |
|
||||
| EMQX v5.0.11 | 21,553 | 17,418 | 14,356 | 4,257 | 3,980 | 3,756 |
|
||||
|
||||
Million Message Challenge (hit the server with 1 million messages immediately):
|
||||
|
||||
`mqtt-stresser -broker tcp://localhost:1883 -num-clients=100 -num-messages=10000`
|
||||
| Broker | publish fastest | median | slowest | receive fastest | median | slowest |
|
||||
| -- | -- | -- | -- | -- | -- | -- |
|
||||
| Mochi v2.2.0 | 51,044 | 4,682 | 2,345 | 72,634 | 7,645 | 2,464 |
|
||||
| Mosquitto v2.0.15 | 3,826 | 3,395 | 3,032 | 1,200 | 1,150 | 1,118 |
|
||||
| EMQX v5.0.11 | 4,086 | 2,432 | 2,274 | 434 | 333 | 311 |
|
||||
|
||||
> Not sure what's going on with EMQX here, perhaps the docker out-of-the-box settings are not optimal, so take it with a pinch of salt as we know for a fact it's a solid piece of software.
|
||||
|
||||
## Stargazers over time 🥰
|
||||
[](https://starchart.cc/mochi-co/mqtt)
|
||||
Are you using Mochi MQTT in a project? [Let us know!](https://github.com/mochi-co/mqtt/issues)
|
||||
|
||||
## Contributions
|
||||
Contributions and feedback are both welcomed and encouraged! [Open an issue](https://github.com/mochi-co/mqtt/issues) to report a bug, ask a question, or make a feature request.
|
||||
|
||||
|
||||
|
||||

|
||||
|
|
@ -5,11 +5,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"github.com/mochi-co/mqtt/v2/packets"
|
||||
"github.com/rs/zerolog"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/mochi-co/mqtt/v2"
|
||||
|
|
@ -17,6 +20,18 @@ import (
|
|||
"github.com/mochi-co/mqtt/v2/listeners"
|
||||
)
|
||||
|
||||
var server = mqtt.New(&mqtt.Options{
|
||||
//Capabilities: &mqtt.Capabilities{
|
||||
// ServerKeepAlive: 10000,
|
||||
// ReceiveMaximum: math.MaxUint16,
|
||||
// MaximumMessageExpiryInterval: math.MaxUint32,
|
||||
// MaximumSessionExpiryInterval: math.MaxUint32, // maximum number of seconds to keep disconnected sessions
|
||||
// MaximumClientWritesPending: 65536,
|
||||
// MaximumPacketSize: 0,
|
||||
// MaximumQos: 2,
|
||||
//},
|
||||
})
|
||||
|
||||
func main() {
|
||||
tcpAddr := flag.String("tcp", ":1883", "network address for TCP listener")
|
||||
wsAddr := flag.String("ws", "", "network address for Websocket listener")
|
||||
|
|
@ -32,17 +47,6 @@ func main() {
|
|||
done <- true
|
||||
}()
|
||||
|
||||
server := mqtt.New(&mqtt.Options{
|
||||
//Capabilities: &mqtt.Capabilities{
|
||||
// ServerKeepAlive: 10000,
|
||||
// ReceiveMaximum: math.MaxUint16,
|
||||
// MaximumMessageExpiryInterval: math.MaxUint32,
|
||||
// MaximumSessionExpiryInterval: math.MaxUint32, // maximum number of seconds to keep disconnected sessions
|
||||
// MaximumClientWritesPending: 65536,
|
||||
// MaximumPacketSize: 0,
|
||||
// MaximumQos: 2,
|
||||
//},
|
||||
})
|
||||
l := server.Log.Level(zerolog.DebugLevel)
|
||||
server.Log = &l
|
||||
|
||||
|
|
@ -89,6 +93,11 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
err := server.AddHook(new(MyHook), map[string]any{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
|
|
@ -100,4 +109,44 @@ func main() {
|
|||
server.Log.Warn().Msg("caught signal, stopping...")
|
||||
server.Close()
|
||||
server.Log.Info().Msg("main.go finished")
|
||||
|
||||
}
|
||||
|
||||
type MyHook struct {
|
||||
mqtt.HookBase
|
||||
}
|
||||
|
||||
func (h *MyHook) ID() string {
|
||||
return "events-controller"
|
||||
}
|
||||
|
||||
func (h *MyHook) Provides(b byte) bool {
|
||||
return bytes.Contains([]byte{
|
||||
mqtt.OnSubscribed,
|
||||
}, []byte{b})
|
||||
}
|
||||
|
||||
func (h *MyHook) Init(config any) error {
|
||||
h.Log.Info().Msg("initialised")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MyHook) OnSubscribed(cl *mqtt.Client, pk packets.Packet, reasonCodes []byte) {
|
||||
// Verifies if it's a device who is subscribed
|
||||
if strings.Contains(pk.Filters[0].Filter, "oktopus/v1/agent") {
|
||||
var clUser string
|
||||
|
||||
if len(cl.Properties.Props.User) > 0 {
|
||||
clUser = cl.Properties.Props.User[0].Val
|
||||
}
|
||||
|
||||
if clUser != "" {
|
||||
log.Println("new device:", clUser)
|
||||
err := server.Publish("oktopus/devices", []byte(clUser), false, 1)
|
||||
if err != nil {
|
||||
log.Println("server publish error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
backend/services/mochi/img.png
Normal file
BIN
backend/services/mochi/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB |
|
|
@ -2,7 +2,7 @@ import { useTheme } from '@mui/material/styles';
|
|||
|
||||
export const Logo = () => {
|
||||
const theme = useTheme();
|
||||
const fillColor = theme.palette.primary.main;
|
||||
const fillColor = '#FFFFFF';
|
||||
|
||||
return (
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import ShoppingBagIcon from '@heroicons/react/24/solid/ShoppingBagIcon';
|
|||
import UserIcon from '@heroicons/react/24/solid/UserIcon';
|
||||
import UserPlusIcon from '@heroicons/react/24/solid/UserPlusIcon';
|
||||
import UsersIcon from '@heroicons/react/24/solid/UsersIcon';
|
||||
import CpuChip from '@heroicons/react/24/solid/CpuChipIcon';
|
||||
import XCircleIcon from '@heroicons/react/24/solid/XCircleIcon';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
|
||||
|
|
@ -19,29 +20,11 @@ export const items = [
|
|||
)
|
||||
},
|
||||
{
|
||||
title: 'Customers',
|
||||
path: '/customers',
|
||||
title: 'Devices',
|
||||
path: '/devices',
|
||||
icon: (
|
||||
<SvgIcon fontSize="small">
|
||||
<UsersIcon />
|
||||
</SvgIcon>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Companies',
|
||||
path: '/companies',
|
||||
icon: (
|
||||
<SvgIcon fontSize="small">
|
||||
<ShoppingBagIcon />
|
||||
</SvgIcon>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Account',
|
||||
path: '/account',
|
||||
icon: (
|
||||
<SvgIcon fontSize="small">
|
||||
<UserIcon />
|
||||
<CpuChip/>
|
||||
</SvgIcon>
|
||||
)
|
||||
},
|
||||
|
|
@ -54,12 +37,24 @@ export const items = [
|
|||
</SvgIcon>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
/*
|
||||
{
|
||||
title: 'Login',
|
||||
path: '/auth/login',
|
||||
title: 'Customers',
|
||||
path: '/customers',
|
||||
icon: (
|
||||
<SvgIcon fontSize="small">
|
||||
<LockClosedIcon />
|
||||
<UsersIcon />
|
||||
</SvgIcon>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Account',
|
||||
path: '/account',
|
||||
icon: (
|
||||
<SvgIcon fontSize="small">
|
||||
<UserIcon />
|
||||
</SvgIcon>
|
||||
)
|
||||
},
|
||||
|
|
@ -72,6 +67,24 @@ export const items = [
|
|||
</SvgIcon>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Login',
|
||||
path: '/auth/login',
|
||||
icon: (
|
||||
<SvgIcon fontSize="small">
|
||||
<LockClosedIcon />
|
||||
</SvgIcon>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Companies',
|
||||
path: '/companies',
|
||||
icon: (
|
||||
<SvgIcon fontSize="small">
|
||||
<ShoppingBagIcon />
|
||||
</SvgIcon>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Error',
|
||||
path: '/404',
|
||||
|
|
@ -81,4 +94,4 @@ export const items = [
|
|||
</SvgIcon>
|
||||
)
|
||||
}
|
||||
];
|
||||
*/
|
||||
|
|
@ -50,7 +50,7 @@ export const SideNavItem = (props) => {
|
|||
justifyContent: 'center',
|
||||
mr: 2,
|
||||
...(active && {
|
||||
color: 'primary.main'
|
||||
color: '#FFFFFF'
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -66,25 +66,13 @@ export const SideNav = (props) => {
|
|||
p: '12px'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Typography
|
||||
color="inherit"
|
||||
variant="subtitle1"
|
||||
>
|
||||
Devias
|
||||
</Typography>
|
||||
<Typography
|
||||
color="neutral.400"
|
||||
variant="body2"
|
||||
>
|
||||
Production
|
||||
</Typography>
|
||||
<div style={{display:'flex',justifyContent:'center'}}>
|
||||
<img src="/assets/oktopus.png" width={'60%'}></img>
|
||||
</div>
|
||||
<SvgIcon
|
||||
fontSize="small"
|
||||
sx={{ color: 'neutral.500' }}
|
||||
>
|
||||
<ChevronUpDownIcon />
|
||||
</SvgIcon>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -78,14 +78,14 @@ export const TopNav = (props) => {
|
|||
direction="row"
|
||||
spacing={2}
|
||||
>
|
||||
<Tooltip title="Contacts">
|
||||
{/*<Tooltip title="Contacts">
|
||||
<IconButton>
|
||||
<SvgIcon fontSize="small">
|
||||
<UsersIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Notifications">
|
||||
</Tooltip>*/}
|
||||
{/*<Tooltip title="Notifications">
|
||||
<IconButton>
|
||||
<Badge
|
||||
badgeContent={4}
|
||||
|
|
@ -97,7 +97,7 @@ export const TopNav = (props) => {
|
|||
</SvgIcon>
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Tooltip>*/}
|
||||
<Avatar
|
||||
onClick={accountPopover.handleOpen}
|
||||
ref={accountPopover.anchorRef}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ const Page = () => {
|
|||
color="text.secondary"
|
||||
variant="body2"
|
||||
>
|
||||
Não possui uma conta?
|
||||
Don't have an account?
|
||||
|
||||
<Link
|
||||
component={NextLink}
|
||||
|
|
@ -111,7 +111,7 @@ const Page = () => {
|
|||
underline="hover"
|
||||
variant="subtitle2"
|
||||
>
|
||||
Registre-se
|
||||
Register
|
||||
</Link>
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
|
|
|||
234
frontend/src/pages/devices.js
Normal file
234
frontend/src/pages/devices.js
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import Head from 'next/head';
|
||||
import { subDays, subHours } from 'date-fns';
|
||||
import { Box, Container, Unstable_Grid2 as Grid } from '@mui/material';
|
||||
import { Layout as DashboardLayout } from 'src/layouts/dashboard/layout';
|
||||
import { OverviewBudget } from 'src/sections/overview/overview-budget';
|
||||
import { OverviewLatestOrders } from 'src/sections/overview/overview-latest-orders';
|
||||
import { OverviewLatestProducts } from 'src/sections/overview/overview-latest-products';
|
||||
import { OverviewSales } from 'src/sections/overview/overview-sales';
|
||||
import { OverviewTasksProgress } from 'src/sections/overview/overview-tasks-progress';
|
||||
import { OverviewTotalCustomers } from 'src/sections/overview/overview-total-customers';
|
||||
import { OverviewTotalProfit } from 'src/sections/overview/overview-total-profit';
|
||||
import { OverviewTraffic } from 'src/sections/overview/overview-traffic';
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const Page = () => (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
Oktopus | TR-369
|
||||
</title>
|
||||
</Head>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
py: 8,
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="xl" >
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
>
|
||||
</Grid>
|
||||
<OverviewLatestOrders
|
||||
orders={[
|
||||
{
|
||||
id: 'f69f88012978187a6c12897f',
|
||||
ref: 'DEV1049',
|
||||
amount: 30.5,
|
||||
customer: {
|
||||
name: 'Ekaterina Tankova'
|
||||
},
|
||||
createdAt: 1555016400000,
|
||||
status: 'Associating'
|
||||
},
|
||||
{
|
||||
id: '9eaa1c7dd4433f413c308ce2',
|
||||
ref: 'DEV1048',
|
||||
amount: 25.1,
|
||||
customer: {
|
||||
name: 'Cao Yu'
|
||||
},
|
||||
createdAt: 1555016400000,
|
||||
status: 'Online'
|
||||
},
|
||||
{
|
||||
id: '01a5230c811bd04996ce7c13',
|
||||
ref: 'DEV1047',
|
||||
amount: 10.99,
|
||||
customer: {
|
||||
name: 'Alexa Richardson'
|
||||
},
|
||||
createdAt: 1554930000000,
|
||||
status: 'Offline'
|
||||
},
|
||||
{
|
||||
id: '1f4e1bd0a87cea23cdb83d18',
|
||||
ref: 'DEV1046',
|
||||
amount: 96.43,
|
||||
customer: {
|
||||
name: 'Anje Keizer'
|
||||
},
|
||||
createdAt: 1554757200000,
|
||||
status: 'Associating'
|
||||
},
|
||||
{
|
||||
id: '9f974f239d29ede969367103',
|
||||
ref: 'DEV1045',
|
||||
amount: 32.54,
|
||||
customer: {
|
||||
name: 'Clarke Gillebert'
|
||||
},
|
||||
createdAt: 1554670800000,
|
||||
status: 'Online'
|
||||
},
|
||||
{
|
||||
id: 'ffc83c1560ec2f66a1c05596',
|
||||
ref: 'DEV1044',
|
||||
amount: 16.76,
|
||||
customer: {
|
||||
name: 'Adam Denisov'
|
||||
},
|
||||
createdAt: 1554670800000,
|
||||
status: 'Online'
|
||||
}
|
||||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
Page.getLayout = (page) => (
|
||||
<DashboardLayout>
|
||||
{page}
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
export default Page;
|
||||
|
||||
/*
|
||||
<OverviewSales
|
||||
chartSeries={[
|
||||
{
|
||||
name: 'This year',
|
||||
data: [18, 16, 5, 8, 3, 14, 14, 16, 17, 19, 18, 20]
|
||||
},
|
||||
{
|
||||
name: 'Last year',
|
||||
data: [12, 11, 4, 6, 2, 9, 9, 10, 11, 12, 13, 13]
|
||||
}
|
||||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
<OverviewLatestProducts
|
||||
products={[
|
||||
{
|
||||
id: '5ece2c077e39da27658aa8a9',
|
||||
image: '/assets/products/product-1.png',
|
||||
name: 'Healthcare Erbology',
|
||||
updatedAt: subHours(now, 6).getTime()
|
||||
},
|
||||
{
|
||||
id: '5ece2c0d16f70bff2cf86cd8',
|
||||
image: '/assets/products/product-2.png',
|
||||
name: 'Makeup Lancome Rouge',
|
||||
updatedAt: subDays(subHours(now, 8), 2).getTime()
|
||||
},
|
||||
{
|
||||
id: 'b393ce1b09c1254c3a92c827',
|
||||
image: '/assets/products/product-5.png',
|
||||
name: 'Skincare Soja CO',
|
||||
updatedAt: subDays(subHours(now, 1), 1).getTime()
|
||||
},
|
||||
{
|
||||
id: 'a6ede15670da63f49f752c89',
|
||||
image: '/assets/products/product-6.png',
|
||||
name: 'Makeup Lipstick',
|
||||
updatedAt: subDays(subHours(now, 3), 3).getTime()
|
||||
},
|
||||
{
|
||||
id: 'bcad5524fe3a2f8f8620ceda',
|
||||
image: '/assets/products/product-7.png',
|
||||
name: 'Healthcare Ritual',
|
||||
updatedAt: subDays(subHours(now, 5), 6).getTime()
|
||||
}
|
||||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={12}
|
||||
md={12}
|
||||
lg={8}
|
||||
>
|
||||
<OverviewLatestOrders
|
||||
orders={[
|
||||
{
|
||||
id: 'f69f88012978187a6c12897f',
|
||||
ref: 'DEV1049',
|
||||
amount: 30.5,
|
||||
customer: {
|
||||
name: 'Ekaterina Tankova'
|
||||
},
|
||||
createdAt: 1555016400000,
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: '9eaa1c7dd4433f413c308ce2',
|
||||
ref: 'DEV1048',
|
||||
amount: 25.1,
|
||||
customer: {
|
||||
name: 'Cao Yu'
|
||||
},
|
||||
createdAt: 1555016400000,
|
||||
status: 'Online'
|
||||
},
|
||||
{
|
||||
id: '01a5230c811bd04996ce7c13',
|
||||
ref: 'DEV1047',
|
||||
amount: 10.99,
|
||||
customer: {
|
||||
name: 'Alexa Richardson'
|
||||
},
|
||||
createdAt: 1554930000000,
|
||||
status: 'refunded'
|
||||
},
|
||||
{
|
||||
id: '1f4e1bd0a87cea23cdb83d18',
|
||||
ref: 'DEV1046',
|
||||
amount: 96.43,
|
||||
customer: {
|
||||
name: 'Anje Keizer'
|
||||
},
|
||||
createdAt: 1554757200000,
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: '9f974f239d29ede969367103',
|
||||
ref: 'DEV1045',
|
||||
amount: 32.54,
|
||||
customer: {
|
||||
name: 'Clarke Gillebert'
|
||||
},
|
||||
createdAt: 1554670800000,
|
||||
status: 'Online'
|
||||
},
|
||||
{
|
||||
id: 'ffc83c1560ec2f66a1c05596',
|
||||
ref: 'DEV1044',
|
||||
amount: 16.76,
|
||||
customer: {
|
||||
name: 'Adam Denisov'
|
||||
},
|
||||
createdAt: 1554670800000,
|
||||
status: 'Online'
|
||||
}
|
||||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
*/
|
||||
46
frontend/src/pages/devices/[id].js
Normal file
46
frontend/src/pages/devices/[id].js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import Head from 'next/head';
|
||||
import { Box, Stack, Typography, Container, Unstable_Grid2 as Grid } from '@mui/material';
|
||||
import { Layout as DashboardLayout } from 'src/layouts/dashboard/layout';
|
||||
import { useRouter } from 'next/router';
|
||||
import { DevicesRPC } from 'src/sections/devices/devices-rpc';
|
||||
|
||||
const Page = () => {
|
||||
const router = useRouter()
|
||||
const { id } = router.query
|
||||
|
||||
|
||||
return(
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
Oktopus | TR-369
|
||||
</title>
|
||||
</Head>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
py: 0,
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg" >
|
||||
<Stack spacing={3} >
|
||||
<Typography variant="h4">
|
||||
RPC
|
||||
</Typography>
|
||||
{/*<SettingsNotifications />*/}
|
||||
< DevicesRPC />
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = (page) => (
|
||||
<DashboardLayout>
|
||||
{page}
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
export default Page;
|
||||
|
|
@ -37,12 +37,6 @@ const Page = () => (
|
|||
sm={6}
|
||||
lg={3}
|
||||
>
|
||||
<OverviewBudget
|
||||
difference={12}
|
||||
positive
|
||||
sx={{ height: '100%' }}
|
||||
value="$24k"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={12}
|
||||
|
|
@ -71,15 +65,62 @@ const Page = () => (
|
|||
sm={6}
|
||||
lg={3}
|
||||
>
|
||||
<OverviewTotalProfit
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={12}
|
||||
lg={4}
|
||||
>
|
||||
<OverviewTraffic
|
||||
chartSeries={[63, 15, 22]}
|
||||
labels={['Cameras', 'Routers', 'Sensors']}
|
||||
sx={{ height: '100%' }}
|
||||
title={'Vendors'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={12}
|
||||
lg={4}
|
||||
>
|
||||
<OverviewTraffic
|
||||
chartSeries={[88, 22]}
|
||||
labels={['Online', 'Offline']}
|
||||
sx={{ height: '100%' }}
|
||||
title={'Status'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={12}
|
||||
//md={6}
|
||||
lg={4}
|
||||
>
|
||||
<OverviewTraffic
|
||||
chartSeries={[63, 15, 22]}
|
||||
labels={['Cameras', 'Routers', 'Sensors']}
|
||||
sx={{ height: '100%' }}
|
||||
value="$15k"
|
||||
title={'Devices Type'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={12}
|
||||
lg={8}
|
||||
md={6}
|
||||
lg={4}
|
||||
>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
Page.getLayout = (page) => (
|
||||
<DashboardLayout>
|
||||
{page}
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
export default Page;
|
||||
|
||||
/*
|
||||
<OverviewSales
|
||||
chartSeries={[
|
||||
{
|
||||
|
|
@ -93,24 +134,7 @@ const Page = () => (
|
|||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={12}
|
||||
md={6}
|
||||
lg={4}
|
||||
>
|
||||
<OverviewTraffic
|
||||
chartSeries={[63, 15, 22]}
|
||||
labels={['Desktop', 'Tablet', 'Phone']}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={12}
|
||||
md={6}
|
||||
lg={4}
|
||||
>
|
||||
<OverviewLatestProducts
|
||||
<OverviewLatestProducts
|
||||
products={[
|
||||
{
|
||||
id: '5ece2c077e39da27658aa8a9',
|
||||
|
|
@ -216,17 +240,4 @@ const Page = () => (
|
|||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
Page.getLayout = (page) => (
|
||||
<DashboardLayout>
|
||||
{page}
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
export default Page;
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const Page = () => (
|
|||
<Typography variant="h4">
|
||||
Settings
|
||||
</Typography>
|
||||
<SettingsNotifications />
|
||||
{/*<SettingsNotifications />*/}
|
||||
<SettingsPassword />
|
||||
</Stack>
|
||||
</Container>
|
||||
|
|
|
|||
110
frontend/src/sections/devices/devices-rpc.js
Normal file
110
frontend/src/sections/devices/devices-rpc.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Divider,
|
||||
Stack,
|
||||
TextField,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
SvgIcon
|
||||
} from '@mui/material';
|
||||
import PaperAirplane from '@heroicons/react/24/solid/PaperAirplaneIcon';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Backdrop from '@mui/material/Backdrop';
|
||||
|
||||
|
||||
export const DevicesRPC = () => {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const [value, setValue] = useState(`
|
||||
{opa,
|
||||
teste123:goiaba}`
|
||||
)
|
||||
|
||||
const [age, setAge] = useState(1);
|
||||
|
||||
const handleChangeRPC = (event) => {
|
||||
setAge(event.target.value);
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
setValue(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card>
|
||||
<CardActions sx={{ justifyContent: 'flex-end'}}>
|
||||
<FormControl sx={{width:'100px'}}>
|
||||
<Select
|
||||
labelId="demo-simple-select-standard-label"
|
||||
id="demo-simple-select-standard"
|
||||
value={age}
|
||||
label="Action"
|
||||
onChange={(event)=>{handleChangeRPC(event)}}
|
||||
variant='standard'
|
||||
>
|
||||
<MenuItem value={1}>Create</MenuItem>
|
||||
<MenuItem value={2}>Read</MenuItem>
|
||||
<MenuItem value={3}>Update</MenuItem>
|
||||
<MenuItem value={4}>Delete</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</CardActions>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Stack
|
||||
spacing={3}
|
||||
alignItems={'stretch'}
|
||||
>
|
||||
<TextField
|
||||
id="outlined-multiline-static"
|
||||
size="large"
|
||||
multiline="true"
|
||||
label="Mensagem"
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
fullWidth
|
||||
rows="10"
|
||||
/>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions sx={{ justifyContent: 'flex-end' }}>
|
||||
<Button variant="contained" endIcon={<SvgIcon><PaperAirplane /></SvgIcon>} onClick={handleOpen}>
|
||||
Send
|
||||
</Button>
|
||||
</CardActions>
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={open}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { format } from 'date-fns';
|
||||
import PropTypes from 'prop-types';
|
||||
import ArrowRightIcon from '@heroicons/react/24/solid/ArrowRightIcon';
|
||||
import ArrowTopRightOnSquareIcon from '@heroicons/react/24/solid/ArrowTopRightOnSquareIcon';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
|
@ -17,36 +18,42 @@ import {
|
|||
} from '@mui/material';
|
||||
import { Scrollbar } from 'src/components/scrollbar';
|
||||
import { SeverityPill } from 'src/components/severity-pill';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const statusMap = {
|
||||
pending: 'warning',
|
||||
delivered: 'success',
|
||||
refunded: 'error'
|
||||
Associating: 'warning',
|
||||
Online: 'success',
|
||||
Offline: 'error'
|
||||
};
|
||||
|
||||
export const OverviewLatestOrders = (props) => {
|
||||
const { orders = [], sx } = props;
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Card sx={sx}>
|
||||
<CardHeader title="Latest Orders" />
|
||||
<CardHeader title="Devices" />
|
||||
<Scrollbar sx={{ flexGrow: 1 }}>
|
||||
<Box sx={{ minWidth: 800 }}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
Order
|
||||
Model
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
Customer
|
||||
</TableCell>
|
||||
<TableCell sortDirection="desc">
|
||||
Date
|
||||
Vendor
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
Status
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
Access
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -72,6 +79,11 @@ export const OverviewLatestOrders = (props) => {
|
|||
{order.status}
|
||||
</SeverityPill>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SvgIcon fontSize="small" sx={{cursor:'pointer'}} onClick={()=>router.push("devices/"+order.id)}>
|
||||
<ArrowTopRightOnSquareIcon />
|
||||
</SvgIcon>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
|
@ -79,21 +91,21 @@ export const OverviewLatestOrders = (props) => {
|
|||
</Table>
|
||||
</Box>
|
||||
</Scrollbar>
|
||||
<Divider />
|
||||
<CardActions sx={{ justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={(
|
||||
<SvgIcon fontSize="small">
|
||||
<ArrowRightIcon />
|
||||
</SvgIcon>
|
||||
)}
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
View all
|
||||
</Button>
|
||||
</CardActions>
|
||||
{/*<Divider />
|
||||
<CardActions sx={{ justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={(
|
||||
<SvgIcon fontSize="small">
|
||||
<ArrowRightIcon />
|
||||
</SvgIcon>
|
||||
)}
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
View all
|
||||
</Button>
|
||||
</CardActions>*/}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import ListBulletIcon from '@heroicons/react/24/solid/ListBulletIcon';
|
||||
import Signal from '@heroicons/react/24/solid/SignalIcon';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
|
|
@ -25,25 +26,25 @@ export const OverviewTasksProgress = (props) => {
|
|||
>
|
||||
<Stack spacing={1}>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
color="sucess.main"
|
||||
gutterBottom
|
||||
variant="overline"
|
||||
>
|
||||
Task Progress
|
||||
Conexão MQTT
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{value}%
|
||||
{value}ms
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: 'warning.main',
|
||||
backgroundColor: '#f28950',
|
||||
height: 56,
|
||||
width: 56
|
||||
}}
|
||||
>
|
||||
<SvgIcon>
|
||||
<ListBulletIcon />
|
||||
<Signal />
|
||||
</SvgIcon>
|
||||
</Avatar>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import ArrowDownIcon from '@heroicons/react/24/solid/ArrowDownIcon';
|
|||
import ArrowUpIcon from '@heroicons/react/24/solid/ArrowUpIcon';
|
||||
import UsersIcon from '@heroicons/react/24/solid/UsersIcon';
|
||||
import { Avatar, Card, CardContent, Stack, SvgIcon, Typography } from '@mui/material';
|
||||
import CpuChipIcon from '@heroicons/react/24/solid/CpuChipIcon';
|
||||
|
||||
export const OverviewTotalCustomers = (props) => {
|
||||
const { difference, positive = false, sx, value } = props;
|
||||
|
|
@ -21,7 +22,7 @@ export const OverviewTotalCustomers = (props) => {
|
|||
color="text.secondary"
|
||||
variant="overline"
|
||||
>
|
||||
Total Customers
|
||||
Total Devices
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{value}
|
||||
|
|
@ -29,13 +30,13 @@ export const OverviewTotalCustomers = (props) => {
|
|||
</Stack>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: 'success.main',
|
||||
backgroundColor: 'indigo.main',
|
||||
height: 56,
|
||||
width: 56
|
||||
}}
|
||||
>
|
||||
<SvgIcon>
|
||||
<UsersIcon />
|
||||
<CpuChipIcon />
|
||||
</SvgIcon>
|
||||
</Avatar>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -14,18 +14,12 @@ import {
|
|||
} from '@mui/material';
|
||||
import { Chart } from 'src/components/chart';
|
||||
|
||||
const useChartOptions = (labels) => {
|
||||
const useChartOptions = (labels,title) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return {
|
||||
let options = {
|
||||
chart: {
|
||||
background: 'transparent'
|
||||
},
|
||||
colors: [
|
||||
theme.palette.primary.main,
|
||||
theme.palette.success.main,
|
||||
theme.palette.warning.main
|
||||
],
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
|
|
@ -60,6 +54,19 @@ const useChartOptions = (labels) => {
|
|||
fillSeriesColor: false
|
||||
}
|
||||
};
|
||||
if(title === "Status"){
|
||||
options.colors = [
|
||||
theme.palette.success.main,
|
||||
theme.palette.error.main,
|
||||
]
|
||||
}else{
|
||||
options.colors = [
|
||||
theme.palette.primary.main,
|
||||
theme.palette.info.main,
|
||||
theme.palette.warning.main
|
||||
]
|
||||
}
|
||||
return options
|
||||
};
|
||||
|
||||
const iconMap = {
|
||||
|
|
@ -81,12 +88,14 @@ const iconMap = {
|
|||
};
|
||||
|
||||
export const OverviewTraffic = (props) => {
|
||||
const { chartSeries, labels, sx } = props;
|
||||
const chartOptions = useChartOptions(labels);
|
||||
const { chartSeries, labels, sx, title } = props;
|
||||
const chartOptions = useChartOptions(labels,title);
|
||||
|
||||
return (
|
||||
<Card sx={sx}>
|
||||
<CardHeader title="Traffic Source" />
|
||||
<div style={{display:'flex',justifyContent:'center'}}>
|
||||
<CardHeader title={title} />
|
||||
</div>
|
||||
<CardContent>
|
||||
<Chart
|
||||
height={300}
|
||||
|
|
@ -117,7 +126,7 @@ export const OverviewTraffic = (props) => {
|
|||
{iconMap[label]}
|
||||
<Typography
|
||||
sx={{ my: 1 }}
|
||||
variant="h6"
|
||||
variant="h8"
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
|
|
|
|||
|
|
@ -12,16 +12,16 @@ const withAlphas = (color) => {
|
|||
};
|
||||
|
||||
export const neutral = {
|
||||
50: '#F8F9FA',
|
||||
50: '#306d6f',
|
||||
100: '#F3F4F6',
|
||||
200: '#E5E7EB',
|
||||
300: '#D2D6DB',
|
||||
400: '#9DA4AE',
|
||||
400: '#FFFFFF',
|
||||
500: '#6C737F',
|
||||
600: '#4D5761',
|
||||
700: '#2F3746',
|
||||
800: '#1C2536',
|
||||
900: '#111927'
|
||||
700: '#FFFFFF',
|
||||
800: '#306d6f',
|
||||
900: '#30596f'
|
||||
};
|
||||
|
||||
export const indigo = withAlphas({
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function createComponents(config) {
|
|||
root: {
|
||||
borderRadius: 20,
|
||||
[`&.${paperClasses.elevation1}`]: {
|
||||
boxShadow: '0px 5px 22px rgba(0, 0, 0, 0.04), 0px 0px 0px 0.5px rgba(0, 0, 0, 0.03)'
|
||||
boxShadow: '0px 5px 22px rgba(0, 0, 0, 0.1), 0px 0px 0px 0.5px rgba(0, 0, 0, 0.03)'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user