diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 605ed4a7..c58790ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,8 +76,26 @@ jobs: go get . go build + gr25: + runs-on: ubuntu-latest + name: gr25 + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + submodules: true + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.22.0' + - name: Build GR25 + run: | + cd gr25 + go get . + go build + all: - needs: [dashboard, auth, jeddah, gr24] + needs: [dashboard, auth, jeddah, gr24, gr25] runs-on: ubuntu-latest name: all steps: diff --git a/docker-compose.yaml b/docker-compose.yaml index 0364f24c..dbed25cf 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,4 @@ -version: "3.9" +name: mapache services: kerbecs: @@ -33,62 +33,32 @@ services: HEARTBEAT_TYPE: "server" HEARTBEAT_INTERVAL: "60" DB_DRIVER: "mysql" - DB_HOST: ${DB_HOST} - DB_PORT: ${DB_PORT} - DB_NAME: ${DB_NAME} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} + DB_HOST: ${DATABASE_HOST} + DB_PORT: ${DATABASE_PORT} + DB_NAME: ${DATABASE_NAME} + DB_USER: ${DATABASE_USER} + DB_PASSWORD: ${DATABASE_PASSWORD} ports: - "10311:10311" - bahrain: - image: gauchoracing/mp_bahrain:latest - container_name: bahrain - restart: unless-stopped - depends_on: - - rincon - environment: - ENV: ${ENV} - PORT: "7002" - AUTH_SIGNING_KEY: ${AUTH_SIGNING_KEY} - DB_HOST: ${DB_HOST} - DB_PORT: ${DB_PORT} - DB_NAME: ${DB_NAME} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} - RINCON_USER: ${RINCON_USER} - RINCON_PASSWORD: ${RINCON_PASSWORD} - ports: - - "7002" - - gr24: - image: gauchoracing/mp_gr24:latest - container_name: gr24 - restart: unless-stopped - depends_on: - - rincon - environment: - ENV: ${ENV} - PORT: "7004" - MQTT_HOST: ${MQTT_HOST} - MQTT_PORT: ${MQTT_PORT} - MQTT_USER: ${MQTT_USER} - MQTT_PASSWORD: ${MQTT_PASSWORD} - DB_HOST: ${DB_HOST} - DB_PORT: ${DB_PORT} - DB_NAME: ${DB_NAME} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} - RINCON_USER: ${RINCON_USER} - RINCON_PASSWORD: ${RINCON_PASSWORD} - ports: - - "7004" - nanomq: image: emqx/nanomq:latest restart: unless-stopped ports: - "1883:1883" + db: + image: ghcr.io/singlestore-labs/singlestoredb-dev:latest + restart: unless-stopped + volumes: + - s2data:/data + - ./singlestore/init.sql:/init.sql + ports: + - "3306:3306" + - "3380:8080" + - "3381:9000" + environment: + ROOT_PASSWORD: "${DATABASE_PASSWORD}" + volumes: - mysql: + s2data: \ No newline at end of file diff --git a/gr25/.gitignore b/gr25/.gitignore new file mode 100644 index 00000000..a0300d6c --- /dev/null +++ b/gr25/.gitignore @@ -0,0 +1,168 @@ +.env + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +coverage.html + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/ + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk \ No newline at end of file diff --git a/gr25/Dockerfile b/gr25/Dockerfile new file mode 100644 index 00000000..52376633 --- /dev/null +++ b/gr25/Dockerfile @@ -0,0 +1,29 @@ +FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder + +RUN apk --no-cache add ca-certificates +RUN apk add --no-cache tzdata + +WORKDIR /app + +COPY go.mod ./ +COPY go.sum ./ +RUN go mod download + +COPY . ./ +ARG TARGETOS +ARG TARGETARCH +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /gr25 + +## +## Deploy +## +FROM alpine:3.21 + +WORKDIR / + +COPY --from=builder /gr25 /gr25 + +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +ENV TZ=America/Los_Angeles + +ENTRYPOINT ["/gr25"] \ No newline at end of file diff --git a/gr25/Makefile b/gr25/Makefile new file mode 100644 index 00000000..ba9bd5d3 --- /dev/null +++ b/gr25/Makefile @@ -0,0 +1,15 @@ +.PHONY: clean run test + +clean: + go clean + go mod tidy + rm *.out + rm coverage.html + +run: + chmod +x scripts/run.sh + ./scripts/run.sh + +test: + chmod +x scripts/test.sh + ./scripts/test.sh diff --git a/gr25/api/api.go b/gr25/api/api.go new file mode 100644 index 00000000..c200c89d --- /dev/null +++ b/gr25/api/api.go @@ -0,0 +1,28 @@ +package api + +import ( + "gr25/config" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func SetupRouter() *gin.Engine { + if config.Env == "PROD" { + gin.SetMode(gin.ReleaseMode) + } + r := gin.Default() + r.Use(cors.New(cors.Config{ + AllowAllOrigins: true, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, + MaxAge: 12 * time.Hour, + AllowCredentials: true, + })) + return r +} + +func InitializeRoutes(router *gin.Engine) { + router.GET("/gr25/ping", Ping) +} diff --git a/gr25/api/ping.go b/gr25/api/ping.go new file mode 100644 index 00000000..954e1f16 --- /dev/null +++ b/gr25/api/ping.go @@ -0,0 +1,12 @@ +package api + +import ( + "gr25/config" + "net/http" + + "github.com/gin-gonic/gin" +) + +func Ping(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": config.Service.FormattedNameWithVersion() + " is online!"}) +} diff --git a/gr25/config/banner.go b/gr25/config/banner.go new file mode 100644 index 00000000..f9d028a3 --- /dev/null +++ b/gr25/config/banner.go @@ -0,0 +1,20 @@ +package config + +import "github.com/fatih/color" + +var Banner = ` +███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗██╗ ██╗███████╗ +████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║██╔════╝ +██╔████╔██║███████║██████╔╝███████║██║ ███████║█████╗ +██║╚██╔╝██║██╔══██║██╔═══╝ ██╔══██║██║ ██╔══██║██╔══╝ +██║ ╚═╝ ██║██║ ██║██║ ██║ ██║╚██████╗██║ ██║███████╗ +╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ +` + +func PrintStartupBanner() { + banner := color.New(color.Bold, color.FgHiMagenta).PrintlnFunc() + banner(Banner) + version := color.New(color.Bold, color.FgMagenta).PrintlnFunc() + version("Running " + Service.FormattedNameWithVersion() + " [ENV: " + Env + "]") + println() +} diff --git a/gr25/config/config.go b/gr25/config/config.go new file mode 100644 index 00000000..7383667f --- /dev/null +++ b/gr25/config/config.go @@ -0,0 +1,37 @@ +package config + +import ( + "os" + + "github.com/bk1031/rincon-go/v2" +) + +var Service rincon.Service = rincon.Service{ + Name: "GR25", + Version: "1.0.0", +} + +var Routes = []rincon.Route{ + { + Route: "/gr25/**", + Method: "*", + }, +} + +var Env = os.Getenv("ENV") +var Port = os.Getenv("PORT") + +var DatabaseHost = os.Getenv("DATABASE_HOST") +var DatabasePort = os.Getenv("DATABASE_PORT") +var DatabaseUser = os.Getenv("DATABASE_USER") +var DatabasePassword = os.Getenv("DATABASE_PASSWORD") +var DatabaseName = os.Getenv("DATABASE_NAME") + +var RinconClient *rincon.Client +var RinconUser = os.Getenv("RINCON_USER") +var RinconPassword = os.Getenv("RINCON_PASSWORD") + +var MQTTHost = os.Getenv("MQTT_HOST") +var MQTTPort = os.Getenv("MQTT_PORT") +var MQTTUser = os.Getenv("MQTT_USER") +var MQTTPassword = os.Getenv("MQTT_PASSWORD") diff --git a/gr25/database/db.go b/gr25/database/db.go new file mode 100644 index 00000000..61cdb1d7 --- /dev/null +++ b/gr25/database/db.go @@ -0,0 +1,38 @@ +package database + +import ( + "fmt" + "gr25/config" + "gr25/utils" + + "time" + + "github.com/gaucho-racing/mapache-go" + singlestore "github.com/singlestore-labs/gorm-singlestore" + "gorm.io/gorm" +) + +var DB *gorm.DB + +var dbRetries = 0 + +func InitializeDB() error { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=UTC", config.DatabaseUser, config.DatabasePassword, config.DatabaseHost, config.DatabasePort, config.DatabaseName) + db, err := gorm.Open(singlestore.Open(dsn), &gorm.Config{}) + if err != nil { + if dbRetries < 5 { + dbRetries++ + utils.SugarLogger.Errorln("failed to connect database, retrying in 5s... ") + time.Sleep(time.Second * 5) + InitializeDB() + } else { + return fmt.Errorf("failed to connect database after 5 attempts") + } + } else { + utils.SugarLogger.Infoln("[DB] Connected to database") + db.AutoMigrate(&mapache.Signal{}) + utils.SugarLogger.Infoln("[DB] AutoMigration complete") + DB = db + } + return nil +} diff --git a/gr25/go.mod b/gr25/go.mod new file mode 100644 index 00000000..a28b8832 --- /dev/null +++ b/gr25/go.mod @@ -0,0 +1,55 @@ +module gr25 + +go 1.23.6 + +require ( + github.com/bk1031/rincon-go/v2 v2.0.0 + github.com/eclipse/paho.mqtt.golang v1.5.0 + github.com/gin-gonic/gin v1.10.0 + go.uber.org/zap v1.27.0 + gorm.io/gorm v1.25.7 +) + +require ( + github.com/bytedance/sonic v1.12.6 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/eclipse/paho.golang v0.22.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.36.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/fatih/color v1.18.0 + github.com/gaucho-racing/mapache-go v1.6.1 + github.com/gin-contrib/cors v1.7.3 + github.com/singlestore-labs/gorm-singlestore v1.2.0 + go.uber.org/multierr v1.10.0 // indirect +) diff --git a/gr25/go.sum b/gr25/go.sum new file mode 100644 index 00000000..e920d8dd --- /dev/null +++ b/gr25/go.sum @@ -0,0 +1,110 @@ +github.com/bk1031/rincon-go/v2 v2.0.0 h1:nmDHQNZI/AFfW+ZGTGoxpNPrv3OYXQ09anX+fCoiQsQ= +github.com/bk1031/rincon-go/v2 v2.0.0/go.mod h1:287Zc8PvUNnJuAwpt9XVuYUL8k4wrXg3Fa3L0KEmAB4= +github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= +github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.golang v0.22.0 h1:JhhUngr8TBlyUZDZw/L6WVayPi9qmSmdWeki48i5AVE= +github.com/eclipse/paho.golang v0.22.0/go.mod h1:9ZiYJ93iEfGRJri8tErNeStPKLXIGBHiqbHV74t5pqI= +github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= +github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/gaucho-racing/mapache-go v1.6.1 h1:VnrTyvr3tOWiMQsIj2Ij+ggNLyeddu2OKITWSGVIFXg= +github.com/gaucho-racing/mapache-go v1.6.1/go.mod h1:I8lpCGdURUpNaDX4JJSWdFDJdjT7i3fMvTvwHICDtqI= +github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns= +github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/singlestore-labs/gorm-singlestore v1.2.0 h1:mccrEa5tyZDvq7LdEl7oIVfizVC0gxkcf2MHoZG1TWQ= +github.com/singlestore-labs/gorm-singlestore v1.2.0/go.mod h1:Bxq1nC7Gr1I7Hb0tS4bF/aLp3/EqD64kY79LeBKYKBQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/gr25/main.go b/gr25/main.go new file mode 100644 index 00000000..f7d31cea --- /dev/null +++ b/gr25/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "gr25/api" + "gr25/config" + "gr25/database" + "gr25/mqtt" + "gr25/service" + "gr25/utils" +) + +func main() { + config.PrintStartupBanner() + utils.InitializeLogger() + utils.VerifyConfig() + defer utils.Logger.Sync() + + service.RegisterRincon() + database.InitializeDB() + mqtt.InitializeMQTT() + service.SubscribeTopics() + + router := api.SetupRouter() + api.InitializeRoutes(router) + err := router.Run(":" + config.Port) + if err != nil { + utils.SugarLogger.Fatalln(err) + } +} diff --git a/gr25/model/ecu.go b/gr25/model/ecu.go new file mode 100644 index 00000000..a86ffe58 --- /dev/null +++ b/gr25/model/ecu.go @@ -0,0 +1,76 @@ +package model + +import mp "github.com/gaucho-racing/mapache-go" + +var ecuStatusOne = mp.Message{ + mp.NewField("ecu_state", 1, mp.Unsigned, mp.BigEndian, nil), + mp.NewField("ecu_status_flags", 3, mp.Unsigned, mp.BigEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + bitMap := []string{ + "ecu_status_acu", + "ecu_status_inv_one", + "ecu_status_inv_two", + "ecu_status_inv_three", + "ecu_status_inv_four", + "ecu_status_fan_one", + "ecu_status_fan_two", + "ecu_status_fan_three", + "ecu_status_fan_four", + "ecu_status_fan_five", + "ecu_status_fan_six", + "ecu_status_fan_seven", + "ecu_status_fan_eight", + "ecu_status_dash", + "ecu_status_steering", + } + for i := 0; i < len(bitMap); i++ { + signals = append(signals, mp.Signal{ + Name: bitMap[i], + Value: float64(f.CheckBit(i)), + RawValue: f.CheckBit(i), + }) + } + return signals + }), + mp.NewField("ecu_maps", 1, mp.Unsigned, mp.BigEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "ecu_power_level", + Value: float64((f.Value >> 4) & 0x0F), + RawValue: (f.Value >> 4) & 0x0F, + }) + signals = append(signals, mp.Signal{ + Name: "ecu_torque_map", + Value: float64(f.Value & 0x0F), + RawValue: f.Value & 0x0F, + }) + return signals + }), + mp.NewField("ecu_max_cell_temp", 1, mp.Unsigned, mp.BigEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "ecu_max_cell_temp", + Value: float64(f.Value) * 0.25, + RawValue: f.Value, + }) + return signals + }), + mp.NewField("ecu_acu_state_of_charge", 1, mp.Unsigned, mp.BigEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "ecu_acu_state_of_charge", + Value: float64(f.Value) * 20 / 51, + RawValue: f.Value, + }) + return signals + }), + mp.NewField("ecu_glv_state_of_charge", 1, mp.Unsigned, mp.BigEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "ecu_glv_state_of_charge", + Value: float64(f.Value) * 20 / 51, + RawValue: f.Value, + }) + return signals + }), +} diff --git a/gr25/model/message.go b/gr25/model/message.go new file mode 100644 index 00000000..3be674f4 --- /dev/null +++ b/gr25/model/message.go @@ -0,0 +1,14 @@ +package model + +import mp "github.com/gaucho-racing/mapache-go" + +var messageMap = map[int]mp.Message{ + 0x003: ecuStatusOne, +} + +func GetMessage(id int) mp.Message { + if msg, ok := messageMap[id]; ok { + return msg + } + return nil +} diff --git a/gr25/mqtt/mqtt.go b/gr25/mqtt/mqtt.go new file mode 100644 index 00000000..a400d8b5 --- /dev/null +++ b/gr25/mqtt/mqtt.go @@ -0,0 +1,25 @@ +package mqtt + +import ( + "fmt" + "gr25/config" + "gr25/utils" + + mq "github.com/eclipse/paho.mqtt.golang" +) + +var Client mq.Client + +func InitializeMQTT() { + opts := mq.NewClientOptions() + opts.AddBroker(fmt.Sprintf("tcp://%s:%s", config.MQTTHost, config.MQTTPort)) + opts.SetUsername(config.MQTTUser) + opts.SetPassword(config.MQTTPassword) + opts.SetAutoReconnect(true) + opts.SetClientID(fmt.Sprintf("%s-%d", config.Service.Name, config.Service.ID)) + Client = mq.NewClient(opts) + if token := Client.Connect(); token.Wait() && token.Error() != nil { + utils.SugarLogger.Fatalln("[MQ] Failed to connect to MQTT", token.Error()) + } + utils.SugarLogger.Infoln("[MQ] Connected to MQTT broker") +} diff --git a/gr25/scripts/deploy.sh b/gr25/scripts/deploy.sh new file mode 100644 index 00000000..67c4b10f --- /dev/null +++ b/gr25/scripts/deploy.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +service_name=$(basename $(dirname $(dirname "$(readlink -f "$0")"))) + +# Extract version from config.go +VERSION=$(grep 'Version: ' config/config.go | cut -d '"' -f 2) + +if [ -z "$VERSION" ] + then + echo "Error: Unable to extract version from config/config.go" + exit 1 +fi + +# Check if docker is installed +if ! [ -x "$(command -v docker)" ]; then + echo 'Error: docker is not installed.' >&2 + exit 1 +fi + +echo "Building container for $service_name v$VERSION" +# Build the docker container +docker build -t gauchoracing/mp_$service_name:"$VERSION" -t gauchoracing/mp_$service_name:latest --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 --push --progress=plain . \ No newline at end of file diff --git a/gr25/scripts/run.sh b/gr25/scripts/run.sh new file mode 100755 index 00000000..1c37094a --- /dev/null +++ b/gr25/scripts/run.sh @@ -0,0 +1,19 @@ +# check if go.mod exists in current directory +if [ ! -f go.mod ]; then + echo "go.mod not found" + echo "Please make sure you are in the root service directory" + exit 1 +fi + +# check if .env exists in current directory +if [ ! -f .env ]; then + echo ".env not found" + echo "Please make sure the .env file is present in the current directory" + exit 1 +fi + + +set -a +. .env +go get . +go run main.go \ No newline at end of file diff --git a/gr25/scripts/test-env.sh b/gr25/scripts/test-env.sh new file mode 100644 index 00000000..3027f954 --- /dev/null +++ b/gr25/scripts/test-env.sh @@ -0,0 +1 @@ +export ENV=PROD \ No newline at end of file diff --git a/gr25/scripts/test.sh b/gr25/scripts/test.sh new file mode 100755 index 00000000..fde6e316 --- /dev/null +++ b/gr25/scripts/test.sh @@ -0,0 +1,17 @@ +# check if go.mod exists in current directory +if [ ! -f go.mod ]; then + echo "go.mod not found" + echo "Please make sure you are in the root ingest directory" + exit 1 +fi + +# check if test-env.sh exists in scripts directory +if [ ! -f scripts/test-env.sh ]; then + echo "scripts/test-env.sh not found" + echo "Please make sure you are in the root ingest directory" + exit 1 +fi + +. scripts/test-env.sh +go test ./... -race -covermode=atomic -coverprofile=coverage.out -v +go tool cover -html coverage.out -o coverage.html \ No newline at end of file diff --git a/gr25/service/message.go b/gr25/service/message.go new file mode 100644 index 00000000..a2e02e7f --- /dev/null +++ b/gr25/service/message.go @@ -0,0 +1,75 @@ +package service + +import ( + "encoding/binary" + "gr25/model" + "gr25/mqtt" + "gr25/utils" + "strconv" + "strings" + "time" + + mq "github.com/eclipse/paho.mqtt.golang" +) + +func SubscribeTopics() { + mqtt.Client.Subscribe("gr25/#", 0, func(client mq.Client, msg mq.Message) { + topic := msg.Topic() + if len(strings.Split(topic, "/")) != 3 { + utils.SugarLogger.Infof("[MQ] Received invalid topic: %s, ignoring", topic) + return + } + vehicleID := strings.Split(topic, "/")[1] + canID := strings.Split(topic, "/")[2] + message := msg.Payload() + canIDInt, err := strconv.ParseInt(canID, 16, 64) + if err != nil { + utils.SugarLogger.Infof("[MQ] Received invalid can id: %s, ignoring", canID) + return + } + utils.SugarLogger.Infof("[MQ] Received message: %s", topic) + go HandleMessage(vehicleID, int(canIDInt), message) + }) +} + +func HandleMessage(vehicleID string, canID int, message []byte) { + // First 8 bytes are timestamp + if len(message) < 10 { // Need at least timestamp (8) + upload key (2) + utils.SugarLogger.Infof("[MQ] Message too short, ignoring") + return + } + timestamp := message[:8] + uploadKey := message[8:10] + data := message[10:] + + // TODO: Check upload key + if int(binary.BigEndian.Uint16(uploadKey)) == 0 { + utils.SugarLogger.Infof("Received invalid upload key: %x, ignoring", uploadKey) + return + } + + messageStruct := model.GetMessage(canID) + if messageStruct == nil { + utils.SugarLogger.Infof("Received unknown message id: %d, ignoring", canID) + return + } + + err := messageStruct.FillFromBytes(data) + if err != nil { + utils.SugarLogger.Infof("Error deserializing message: %s", err) + return + } + + signals := messageStruct.ExportSignals() + for _, signal := range signals { + signal.Timestamp = int(binary.BigEndian.Uint64(timestamp)) + signal.VehicleID = vehicleID + signal.ProducedAt = time.UnixMicro(int64(signal.Timestamp)) + signal.CreatedAt = utils.WithPrecision(time.Now()) + + err := CreateSignal(signal) + if err != nil { + utils.SugarLogger.Infof("Error creating signal: %s", err) + } + } +} diff --git a/gr25/service/rincon.go b/gr25/service/rincon.go new file mode 100644 index 00000000..7d1c3d49 --- /dev/null +++ b/gr25/service/rincon.go @@ -0,0 +1,71 @@ +package service + +import ( + "gr25/config" + "gr25/utils" + "strings" + "time" + + "github.com/bk1031/rincon-go/v2" +) + +var rinconRetries = 0 +var isRunningInDocker = false + +func RegisterRincon() { + if config.RinconUser == "" || config.RinconPassword == "" { + utils.SugarLogger.Debugln("Rincon user or password is not set, skipping registration") + return + } + rinconEndpoint := "http://rincon:10311" + client, err := rincon.NewClient(rincon.Config{ + BaseURL: rinconEndpoint, + HeartbeatMode: rincon.ServerHeartbeat, + HeartbeatInterval: 60, + AuthUser: config.RinconUser, + AuthPassword: config.RinconPassword, + }) + if err != nil { + utils.SugarLogger.Errorf("Failed to create Rincon client with %s: %v", rinconEndpoint, err) + rinconEndpoint = "http://localhost:10311" + client, err = rincon.NewClient(rincon.Config{ + BaseURL: rinconEndpoint, + HeartbeatMode: rincon.ServerHeartbeat, + HeartbeatInterval: 60, + AuthUser: config.RinconUser, + AuthPassword: config.RinconPassword, + }) + if err != nil { + if rinconRetries < 5 { + utils.SugarLogger.Errorf("Failed to create Rincon client with %s: %v, retrying in 5s...", rinconEndpoint, err) + rinconRetries++ + time.Sleep(time.Second * 5) + RegisterRincon() + } else { + utils.SugarLogger.Fatalln("Failed to create Rincon client after 5 attempts") + return + } + } else { + utils.SugarLogger.Infof("Created Rincon client with endpoint %s", rinconEndpoint) + isRunningInDocker = false + } + } else { + utils.SugarLogger.Infof("Created Rincon client with endpoint %s", rinconEndpoint) + isRunningInDocker = true + } + config.RinconClient = client + if isRunningInDocker { + config.Service.Endpoint = "http://gr25:" + config.Port + config.Service.HealthCheck = "http://gr25:" + config.Port + "/" + strings.ToLower(config.Service.Name) + "/ping" + } else { + config.Service.Endpoint = "http://host.docker.internal:" + config.Port + config.Service.HealthCheck = "http://host.docker.internal:" + config.Port + "/" + strings.ToLower(config.Service.Name) + "/ping" + } + id, err := config.RinconClient.Register(config.Service, config.Routes) + if err != nil { + utils.SugarLogger.Errorf("Failed to register service with Rincon: %v", err) + return + } + config.Service = *config.RinconClient.Service() + utils.SugarLogger.Infof("Registered service with ID: %d", id) +} diff --git a/gr25/service/signal.go b/gr25/service/signal.go new file mode 100644 index 00000000..f59f6bba --- /dev/null +++ b/gr25/service/signal.go @@ -0,0 +1,44 @@ +package service + +import ( + "fmt" + "gr25/database" + "gr25/utils" + + "github.com/gaucho-racing/mapache-go" +) + +func GetSignal(timestamp int, vehicleID string, name string) mapache.Signal { + var signal mapache.Signal + database.DB.Where("timestamp = ?", timestamp).Where("vehicle_id = ?", vehicleID).Where("name = ?", name).First(&signal) + return signal +} + +func CreateSignal(signal mapache.Signal) error { + if signal.Timestamp == 0 { + return fmt.Errorf("signal timestamp cannot be 0") + } + if signal.VehicleID == "" { + return fmt.Errorf("signal vehicle id cannot be empty") + } + if signal.Name == "" { + return fmt.Errorf("signal name cannot be empty") + } + if database.DB.Where("timestamp = ?", signal.Timestamp).Where("vehicle_id = ?", signal.VehicleID).Where("name = ?", signal.Name).Updates(&signal).RowsAffected == 0 { + utils.SugarLogger.Infow("[DB] New signal created", + "timestamp", signal.Timestamp, + "vehicle_id", signal.VehicleID, + "name", signal.Name, + ) + if result := database.DB.Create(&signal); result.Error != nil { + return result.Error + } + } else { + utils.SugarLogger.Infow("[DB] Existing signal updated", + "timestamp", signal.Timestamp, + "vehicle_id", signal.VehicleID, + "name", signal.Name, + ) + } + return nil +} diff --git a/gr25/utils/config.go b/gr25/utils/config.go new file mode 100644 index 00000000..240a9a0a --- /dev/null +++ b/gr25/utils/config.go @@ -0,0 +1,26 @@ +package utils + +import "gr25/config" + +func VerifyConfig() { + if config.Port == "" { + config.Port = "9999" + SugarLogger.Infof("PORT is not set, defaulting to %s", config.Port) + } + if config.DatabaseHost == "" { + config.DatabaseHost = "localhost" + SugarLogger.Infof("DATABASE_HOST is not set, defaulting to %s", config.DatabaseHost) + } + if config.DatabasePort == "" { + config.DatabasePort = "3306" + SugarLogger.Infof("DATABASE_PORT is not set, defaulting to %s", config.DatabasePort) + } + if config.DatabaseUser == "" { + config.DatabaseUser = "root" + SugarLogger.Infof("DATABASE_USER is not set, defaulting to %s", config.DatabaseUser) + } + if config.DatabasePassword == "" { + config.DatabasePassword = "password" + SugarLogger.Infof("DATABASE_PASSWORD is not set, defaulting to %s", config.DatabasePassword) + } +} diff --git a/gr25/utils/logger.go b/gr25/utils/logger.go new file mode 100644 index 00000000..0e5c42e3 --- /dev/null +++ b/gr25/utils/logger.go @@ -0,0 +1,18 @@ +package utils + +import ( + "gr25/config" + + "go.uber.org/zap" +) + +var Logger *zap.Logger +var SugarLogger *zap.SugaredLogger + +func InitializeLogger() { + Logger = zap.Must(zap.NewProduction()) + if config.Env == "DEV" { + Logger = zap.Must(zap.NewDevelopment()) + } + SugarLogger = Logger.Sugar() +} diff --git a/gr25/utils/time.go b/gr25/utils/time.go new file mode 100644 index 00000000..d6899ed0 --- /dev/null +++ b/gr25/utils/time.go @@ -0,0 +1,11 @@ +package utils + +import ( + "math" + "time" +) + +func WithPrecision(t time.Time) time.Time { + round := time.Second / time.Duration(math.Pow10(6)) + return t.Round(round) +} diff --git a/singlestore/init.sql b/singlestore/init.sql new file mode 100644 index 00000000..edda1fbc --- /dev/null +++ b/singlestore/init.sql @@ -0,0 +1 @@ +CREATE DATABASE IF NOT EXISTS mapache;