diff --git a/.env b/.env index e7ce3b1..6a27ebd 100644 --- a/.env +++ b/.env @@ -14,4 +14,7 @@ POSTGRES_DB=moxitech POSTGRES_USER=moxitech POSTGRES_PASSWORD=moxitech POSTGRES_PORT=5432 -POSTGRES_HOST=postgres \ No newline at end of file +POSTGRES_HOST=postgres + +SOCKET_BASE_ADDRESS=9091 +SOCKET_SERVICE_HOST=socket \ No newline at end of file diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..c58909a --- /dev/null +++ b/DOCS.md @@ -0,0 +1,7 @@ + + +SOCKET MESSAGING: + // PORT CHANGE IN .env + localhost:9091/storeEvent?client_id=1&message={"me": "update"} + // CONNECTION: + ws://localhost:9091/ws/1 \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 45e9a18..95f6bee 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -64,6 +64,18 @@ services: networks: - dns_net restart: always + + socket: + container_name: dns-socket + build: + context: ./src/socket + dockerfile: Dockerfile + env_file: ".env" + ports: + - "${SOCKET_BASE_ADDRESS}:${SOCKET_BASE_ADDRESS}" + networks: + - dns_net + restart: always front: container_name: dns-ui diff --git a/image.png b/image.png new file mode 100644 index 0000000..3ad6737 Binary files /dev/null and b/image.png differ diff --git a/src/frontend/src/Services/SocketMessaging/MessageHook.js b/src/frontend/src/Services/SocketMessaging/MessageHook.js new file mode 100644 index 0000000..4dafeae --- /dev/null +++ b/src/frontend/src/Services/SocketMessaging/MessageHook.js @@ -0,0 +1,49 @@ +import { useEffect, useState, useRef } from 'react'; + +const useWebSocket = (userToken) => { + const [message, setMessage] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const ws = useRef(null); + + useEffect(() => { + if (!userToken) return; + + const connectWebSocket = () => { + const socketUrl = `ws://${process.env.SOCKET_SERVICE_HOST}:${process.env.SOCKET_BASE_ADDRESS}/ws/${userToken}`; + ws.current = new WebSocket(socketUrl); + + ws.current.onopen = () => { + console.log('WebSocket connected'); + setIsConnected(true); + }; + + ws.current.onmessage = (event) => { + const data = event.data; + console.log('Message received:', data); + setMessage(data); + }; + + ws.current.onclose = () => { + console.log('WebSocket disconnected, attempting to reconnect...'); + setIsConnected(false); + setTimeout(connectWebSocket, 3000); // Reconnect after 3 seconds + }; + + ws.current.onerror = (error) => { + console.error('WebSocket error:', error); + }; + }; + + connectWebSocket(); + + return () => { + if (ws.current) { + ws.current.close(); + } + }; + }, [userToken]); + + return { message, isConnected }; +}; + +export default useWebSocket; \ No newline at end of file diff --git a/src/frontend/src/Services/WebsocketHook.js b/src/frontend/src/Services/WebsocketHook.js index 66480bc..807cb93 100644 --- a/src/frontend/src/Services/WebsocketHook.js +++ b/src/frontend/src/Services/WebsocketHook.js @@ -100,7 +100,19 @@ const useWebsocketConnection = (userToken, roomHash) => { return { websocketStruct, sendMessage }; }; -export const spawnJsonSignal = (signal, name, coords) => { +export const spawnJsonSignal = + ( signal, + name, + coords, + antennaRadius, + antennaDirections="0,0,0", + modulation="", + bandwidth=0, + dataRate=0, + wayPoints=[], + speed=0, + meshName=null + ) => { if (![1, 2].includes(signal)) { throw new Error('Invalid signal value. Expected 1 or 2.'); } @@ -115,11 +127,20 @@ export const spawnJsonSignal = (signal, name, coords) => { name, params: { coords, + antennaRadius, + antennaDirections, + modulation, + bandwidth, + dataRate, + wayPoints, + speed, + meshName }, }, }; }; + export const DeleteSignal = (name) => { const signal = 3; diff --git a/src/frontend/src/DeviceGroups.css b/src/frontend/src/css/DeviceGroups.css similarity index 100% rename from src/frontend/src/DeviceGroups.css rename to src/frontend/src/css/DeviceGroups.css diff --git a/src/frontend/src/Devices.css b/src/frontend/src/css/Devices.css similarity index 100% rename from src/frontend/src/Devices.css rename to src/frontend/src/css/Devices.css diff --git a/src/frontend/src/pages/Dashboard.js b/src/frontend/src/pages/Dashboard.js index e0676ec..a5f1cf2 100644 --- a/src/frontend/src/pages/Dashboard.js +++ b/src/frontend/src/pages/Dashboard.js @@ -28,14 +28,23 @@ const Dashboard = () => { const [objectType, setObjectType] = useState(1); // 1 = Drone, 2 = Base Station const [formObjectData, setFormObjectData] = useState({ name: "", - type: 1, - coordinates_x: 0, - coordinates_y: 0, - coordinates_z: 0, - droneField1: "", - droneField2: "", - baseStationField1: "", - baseStationField2: "" + type: 0, // 1 = Drone, 2 = Base Station + coordinates_x: 0, // Координата X на карте + coordinates_y: 0, // Координата Y на карте + coordinates_z: 0, // Координата Z на карте + + antennaRadius: 0, // Радиус антенны + antennaDirection_x: 0,// Направление антенны по X + antennaDirection_y: 0,// Направление антенны по Y + antennaDirection_z: 0,// Направление антенны по Z + + modulation: "", // Модуляция + bandwidth: 0, // Пропускная способность + dataRate: 0, // Скорость передачи данных + // DRONE FIELDS + wayPoints: 0, // Точки следования + speed: 0, // Скорость + meshName: "", // Название сети }); const [searchTerm, setSearchTerm] = useState(""); const handleSearch = () => { @@ -76,18 +85,13 @@ const Dashboard = () => { alert("Объект с указанным именем не найден"); } }; - - const handleTypeChange = (type) => { setObjectType(type); setFormData({ ...formData, type }); }; - const handleDeleteSignal = (name) => { sendMessage(DeleteSignal(name)); }; - - // Одно состояние для управления всеми модальными окнами const [modals, setModals] = useState({ isModalOpen: false, @@ -96,7 +100,6 @@ const Dashboard = () => { isModalSearchOpen: false, isModalMapOpen: false, }); - // Функция открытия модального окна в зависимости от переданной строки const openModal = (modalName) => { setModals((prevModals) => ({ @@ -104,7 +107,6 @@ const Dashboard = () => { [modalName]: true, })); }; - // Функция закрытия всех модальных окон const closeAllModals = () => { setModals({ @@ -114,7 +116,7 @@ const Dashboard = () => { isModalSearchOpen: false, }); }; - + // Обработка websocket useEffect(() => { if (websocketStruct && websocketStruct.modulation) { const modulation = websocketStruct.modulation; @@ -327,9 +329,34 @@ const containsObjectRecursively = (parent, target) => { sceneRef.current.add(drone.getObject()); } }; - // Добавление дрона в сцену -// Добавление базовой станции в сцену -const handleAddBaseStation = () => { + const modalAddDroneOrBaseStation = () => { + //formObjectData + alert(`Добавлено: ${JSON.stringify(formObjectData)}`); + if (sceneRef.current) { + if (objectType === 1) { + const drone = new Drone(formObjectData.name); + drone.setPosition(formObjectData.coordinates_x, formObjectData.coordinates_y, formObjectData.coordinates_z); + setDrones((prevDrones) => [...prevDrones, drone]); + let json_signal = spawnJsonSignal( + 1, + formObjectData.name, + `${formObjectData.coordinates_x},${formObjectData.coordinates_y},${formObjectData.coordinates_z}`, + formObjectData.antennaRadius, + `${formObjectData.antennaDirection_x},${formObjectData.antennaDirection_y},${formObjectData.antennaDirection_z}`, + formObjectData.modulation, + formObjectData.bandwidth, + formObjectData.dataRate, + formObjectData.wayPoints, + formObjectData.speed, + formObjectData.meshName + ); + sendMessage(json_signal) + sceneRef.current.add(drone.getObject()); + } + } + } + // Добавление базовой станции в сцену + const handleAddBaseStation = () => { if (sceneRef.current) { const current = Date.now(); const base = new BaseStation(current / 1000); @@ -350,7 +377,7 @@ const handleAddBaseStation = () => { console.log(baseStation.map(b => b.getObject().children[0])); sceneRef.current.add(base.getObject()); } -}; + }; // Обработчик ввода на форму высот const handleInputChange = (event) => { @@ -497,60 +524,120 @@ const handleAddBaseStation = () => { /> +
+ +
+ Радиус антенны + +
+
+ Направление X + +
+
+ Направление Y + +
+
+ Направление Z + +
+
+ Модуляция + +
+
+ Пропускная способность + +
+
+ Скорость + +
+ +
{objectType === 1 && ( <>
- +
- + +
+
+ +
)} - {objectType === 2 && ( - <> -
- - -
-
- - -
- - )}
- +
diff --git a/src/frontend/src/pages/DeviceGroups.js b/src/frontend/src/pages/DeviceGroups.js index 8c2e3a2..c8e268a 100644 --- a/src/frontend/src/pages/DeviceGroups.js +++ b/src/frontend/src/pages/DeviceGroups.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import '../DeviceGroups.css'; +import '../css/DeviceGroups.css'; const DeviceGroups = () => { const [groups, setGroups] = useState([]); diff --git a/src/frontend/src/pages/Devices.js b/src/frontend/src/pages/Devices.js index 346abe6..3b0f2aa 100644 --- a/src/frontend/src/pages/Devices.js +++ b/src/frontend/src/pages/Devices.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { QRCodeCanvas } from "qrcode.react"; -import '../Devices.css'; +import '../css/Devices.css'; const Devices = () => { return ( diff --git a/src/frontend/src/pages/ex.js b/src/frontend/src/pages/ex.js deleted file mode 100644 index b4a1359..0000000 --- a/src/frontend/src/pages/ex.js +++ /dev/null @@ -1,33 +0,0 @@ - -// Инициализация WebSocket соединения -// const socket = io("ws://localhost:8080"); - - - // useEffect(() => { - // // Получаем местоположение пользователя - // if (navigator.geolocation) { - // navigator.geolocation.getCurrentPosition( - // (position) => { - // const { latitude, longitude } = position.coords; - // setUserLocation([latitude, longitude]); // Устанавливаем координаты пользователя - // setLocationLoaded(true); // Флаг, что местоположение загружено - // }, - // (error) => { - // console.error("Ошибка при получении геолокации:", error); - // setLocationLoaded(true); // Продолжаем даже при ошибке - // } - // ); - // } else { - // console.error("Геолокация не поддерживается вашим браузером"); - // setLocationLoaded(true); // Продолжаем даже если геолокация недоступна - // } - - // // Получаем данные устройств в реальном времени - // socket.on("deviceLocationUpdate", (data) => { - // setDevices(data); - // }); - - // return () => { - // socket.off("deviceLocationUpdate"); - // }; - // }, []); \ No newline at end of file diff --git a/src/frontend/src/pages/model/BaseStation.js b/src/frontend/src/pages/model/BaseStation.js index 01e9f54..873faab 100644 --- a/src/frontend/src/pages/model/BaseStation.js +++ b/src/frontend/src/pages/model/BaseStation.js @@ -1,9 +1,26 @@ import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; class BaseStation { - constructor(name, scale = 10) { + constructor( + name, + scale = 10, + antennaRadius = 1, + antennaDirection_x=0, + antennaDirection_y=0, + antennaDirection_z=0, + modulation="QAM-16", + bandwidth=0, + dataRate=0, + ) { this.object = new THREE.Object3D(); this.name = name; + this.antennaRadius = antennaRadius; + this.antennaDirection_x = antennaDirection_x; + this.antennaDirection_y = antennaDirection_y; + this.antennaDirection_z = antennaDirection_z; + this.modulation = modulation; + this.bandwidth = bandwidth; + this.dataRate = dataRate; // Инициализируем GLTFLoader const loader = new GLTFLoader(); diff --git a/src/frontend/src/pages/model/Drone.js b/src/frontend/src/pages/model/Drone.js index 16d4b7d..82c54cf 100644 --- a/src/frontend/src/pages/model/Drone.js +++ b/src/frontend/src/pages/model/Drone.js @@ -1,10 +1,32 @@ import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; class Drone { - constructor(name, scale = 0.5) { + constructor(name, + antennaRadius = 1, + antennaDirection_x=0, + antennaDirection_y=0, + antennaDirection_z=0, + modulation="QAM-16", + bandwidth=0, + dataRate=0, + wayPoints=[], + speed=0, + meshName=null, + scale = 0.5) + { this.object = new THREE.Object3D(); this.name = name; + this.antennaRadius = antennaRadius; + this.antennaDirection_x = antennaDirection_x; + this.antennaDirection_y = antennaDirection_y; + this.antennaDirection_z = antennaDirection_z; + this.modulation = modulation; + this.bandwidth = bandwidth; + this.dataRate = dataRate; + this.wayPoints = wayPoints + this.speed = speed; + this.meshName = meshName; // Инициализируем GLTFLoader const loader = new GLTFLoader(); diff --git a/src/server/internal/database/mongo.go b/src/server/internal/database/mongo.go index dfe5c3d..f6cbba2 100644 --- a/src/server/internal/database/mongo.go +++ b/src/server/internal/database/mongo.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -102,3 +103,65 @@ func (db *MongoDbInstance) InsertIntoSimulationsHistory(data interface{}) (primi return insertedID, nil } + +// MapUpload saves a [][]float64 map into the map_templates collection +func (db *MongoDbInstance) MapUpload(mapData [][]float64) (primitive.ObjectID, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + collection := db.Db.Collection("map_templates") + + mapDocument := bson.M{"map_data": mapData, "created_at": time.Now()} + + result, err := collection.InsertOne(ctx, mapDocument) + if err != nil { + log.Printf("Error inserting map into collection 'map_templates': %v", err) + return primitive.NilObjectID, err + } + + insertedID := result.InsertedID.(primitive.ObjectID) + log.Printf("Map successfully inserted with ID: %v", insertedID) + + return insertedID, nil +} + +// MapLoader loads a [][]float64 map from the map_templates collection +func (db *MongoDbInstance) MapLoader(mapID primitive.ObjectID) ([][]float64, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + collection := db.Db.Collection("map_templates") + + var result bson.M + err := collection.FindOne(ctx, bson.M{"_id": mapID}).Decode(&result) + if err != nil { + log.Printf("Error loading map from collection 'map_templates': %v", err) + return nil, err + } + + mapData, ok := result["map_data"].(primitive.A) + if !ok { + log.Printf("Error asserting map data type") + return nil, mongo.ErrNoDocuments + } + + heightMap := make([][]float64, len(mapData)) + for i, row := range mapData { + rowArray, ok := row.(primitive.A) + if !ok { + log.Printf("Error asserting row type") + return nil, mongo.ErrNoDocuments + } + heightMap[i] = make([]float64, len(rowArray)) + for j, val := range rowArray { + floatVal, ok := val.(float64) + if !ok { + log.Printf("Error asserting value type at row %d, column %d", i, j) + return nil, mongo.ErrNoDocuments + } + heightMap[i][j] = floatVal + } + } + + return heightMap, nil +} diff --git a/src/server/internal/database/postgres.go b/src/server/internal/database/postgres.go index 8b02534..9118033 100644 --- a/src/server/internal/database/postgres.go +++ b/src/server/internal/database/postgres.go @@ -57,6 +57,11 @@ func (db *GormDbInstance) AutoMakeSuperUser() error { db.DB.Create(&database.User{Username: "admin", Password: "admin", IsAdmin: true}) fmt.Println("Superuser has updated! :: \n Login: admin \n Password: admin") } + db.DB.Where("username = ?", "user").First(&user) + if user.ID == 0 { + db.DB.Create(&database.User{Username: "user", Password: "user", IsAdmin: false}) + fmt.Println("User has added! :: \n Login: user \n Password: user") + } return nil } else { return fmt.Errorf("DB is nil poiner") diff --git a/src/server/package/math/simulator/simulator.go b/src/server/package/math/simulator/simulator.go index 4aa7b0a..91b9970 100644 --- a/src/server/package/math/simulator/simulator.go +++ b/src/server/package/math/simulator/simulator.go @@ -1,10 +1,14 @@ package simulator import ( + "log" "math" "math/rand" + "moxitech/dns/internal/database" "moxitech/dns/package/math/distance" "sync" + + "go.mongodb.org/mongo-driver/bson/primitive" ) type Map struct { @@ -246,11 +250,18 @@ func CalculateDataRate(modulation string, bandwidth float64) float64 { // MakeExampleMap создает статическую карту func MakeExampleMap() Map { + mapID, err := primitive.ObjectIDFromHex("6709c34cdeb5f8e46f450c9f") + if err != nil { + log.Fatalf("Ошибка при преобразовании строки в ObjectID: %v", err) + } + + database.Instance.MapLoader(mapID) heightData := [][]float64{ {0, 10, 15, 20}, {5, 15, 25, 30}, {10, 20, 35, 40}, {15, 25, 40, 50}, + {20, 30, 25, 50}, } // Определяем карту высот @@ -263,7 +274,7 @@ func MakeExampleMap() Map { return mapObj } -// Создает полностью случаную карту +// Создает полностью случайную карту func MakeRandomMap() Map { // Задаём случайное количество высот numRows := rand.Intn(50) + 1 // Случайное количество строк (от 1 до 50) diff --git a/src/socket/Dockerfile b/src/socket/Dockerfile new file mode 100644 index 0000000..041f1dd --- /dev/null +++ b/src/socket/Dockerfile @@ -0,0 +1,12 @@ +# Build stage +FROM golang:1.22.7-alpine3.20 AS builder +WORKDIR /var/socket +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o main cmd/main.go + +# Run stage +FROM alpine:3.20.3 +COPY --from=builder /var/socket/main /main +CMD ["./main"] diff --git a/src/socket/cmd/main.go b/src/socket/cmd/main.go new file mode 100644 index 0000000..dcd1ca2 --- /dev/null +++ b/src/socket/cmd/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +var ( + clients = make(map[int]*websocket.Conn) // Список активных клиентов + events = make(map[int]string) // Локальная карта для хранения сообщений + mutex sync.Mutex // Мьютекс для управления конкурентным доступом к карте + upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + } +) + +func main() { + http.HandleFunc("/storeEvent", handleStoreEvent) + http.HandleFunc("/ws/", handleWebSocket) + + // Запуск горутины для обработки сообщений каждую секунду + go processEvents() + fmt.Printf("Server started on %v \n", os.Getenv("SOCKET_BASE_ADDRESS")) + http.ListenAndServe(fmt.Sprintf("0.0.0.0:%v", os.Getenv("SOCKET_BASE_ADDRESS")), nil) +} + +// Обработчик для сохранения событий +func handleStoreEvent(w http.ResponseWriter, r *http.Request) { + clientID := r.URL.Query().Get("client_id") + message := r.URL.Query().Get("message") + + if clientID == "" || message == "" { + http.Error(w, "client_id and message are required", http.StatusBadRequest) + return + } + + var id int + _, err := fmt.Sscanf(clientID, "%d", &id) + if err != nil { + http.Error(w, "Invalid client_id", http.StatusBadRequest) + return + } + + mutex.Lock() + events[id] = message + mutex.Unlock() + + w.WriteHeader(http.StatusOK) +} + +// Обработчик WebSocket соединений +func handleWebSocket(w http.ResponseWriter, r *http.Request) { + clientIDStr := r.URL.Path[len("/ws/"):] + var clientID int + _, err := fmt.Sscanf(clientIDStr, "%d", &clientID) + if err != nil { + http.Error(w, "Invalid client ID", http.StatusBadRequest) + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + http.Error(w, "Failed to upgrade to WebSocket", http.StatusInternalServerError) + return + } + + mutex.Lock() + clients[clientID] = conn + mutex.Unlock() +} + +// Функция для обработки событий и отправки сообщений подключенным клиентам +func processEvents() { + for { + time.Sleep(1 * time.Second) + + mutex.Lock() + for id, message := range events { + if conn, ok := clients[id]; ok { + err := conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Client %d: %s", id, message))) + if err != nil { + fmt.Printf("Error sending message to client %d: %v\n", id, err) + conn.Close() + delete(clients, id) + } + // Удаляем событие после отправки + delete(events, id) + } + } + mutex.Unlock() + } +} diff --git a/src/socket/go.mod b/src/socket/go.mod new file mode 100644 index 0000000..b5e83fb --- /dev/null +++ b/src/socket/go.mod @@ -0,0 +1,5 @@ +module moxitech/socket + +go 1.22.7 + +require github.com/gorilla/websocket v1.5.3 // indirect diff --git a/src/socket/go.sum b/src/socket/go.sum new file mode 100644 index 0000000..25a9fc4 --- /dev/null +++ b/src/socket/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=