server & frontend update :: signaling

This commit is contained in:
moxitech 2024-10-11 08:12:41 +07:00
parent a3d344ee2f
commit 047a3ffd5c
15 changed files with 361 additions and 107 deletions

View File

@ -71,6 +71,10 @@ services:
context: ./src/frontend
dockerfile: Dockerfile
env_file: ".env"
volumes:
- ./src/frontend:/app
working_dir: /app
command: ["npm", "start"]
ports:
- "3000:3000"
networks:

View File

@ -11,7 +11,7 @@ COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
# COPY . .
# Make port 3000 available to the world outside this container
EXPOSE 3000

View File

@ -12,6 +12,7 @@ import Docs from './pages/Docs'; // Импортируем страницу п
import './css/bootstrap-5.3.3-dist/css/bootstrap.min.css';
import './App.css';
import "bootstrap-icons/font/bootstrap-icons.css";
import {removeCustomCookie} from './Services/Local'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
const App = () => {
@ -20,6 +21,7 @@ const App = () => {
// Функция для выхода из системы
const handleLogout = () => {
removeCustomCookie("userToken")
setIsLoggedIn(false);
};

View File

@ -88,8 +88,9 @@ const useWebsocketConnection = (userToken, roomHash) => {
};
// Функция для отправки данных в WebSocket
const sendMessage = (message) => {
const sendMessage = async (message) => {
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
console.log(JSON.stringify(message))
socketRef.current.send(JSON.stringify(message));
} else {
console.error('WebSocket is not open. Unable to send message:', message);
@ -99,4 +100,42 @@ const useWebsocketConnection = (userToken, roomHash) => {
return { websocketStruct, sendMessage };
};
export const spawnJsonSignal = (signal, name, coords) => {
if (![1, 2].includes(signal)) {
throw new Error('Invalid signal value. Expected 1 or 2.');
}
if (typeof name !== 'string' || !name.trim()) {
throw new Error('Invalid name. Expected a non-empty string.');
}
return {
signal,
data: {
name,
params: {
coords,
},
},
};
};
export const DeleteSignal = (name) => {
const signal = 3;
if (typeof name !== 'string' || !name.trim()) {
throw new Error('Invalid name. Expected a non-empty string.');
}
return {
signal,
data: {
name,
},
};
};
export default useWebsocketConnection;

View File

@ -1,6 +1,6 @@
.user-account {
padding: 20px;
background-color: #f4f4f4;
background-color: #3cafb7;
border-radius: 10px;
max-width: 400px;
margin: 0 auto;

View File

@ -6,16 +6,15 @@ import BaseStation from './model/BaseStation';
import HeightPoint from './model/HeightPoint';
import { RandomHeightPoint } from './helpers/generateRandomHeightPoints'
import ModalWindow from './components/Modal/Modal';
import useWebsocketConnection from '../Services/WebsocketHook';
import { getCustomCookie } from '../Services/Local';
import useWebsocketConnection, { DeleteSignal, spawnJsonSignal } from '../Services/WebsocketHook';
import './Dashboard.css';
const Dashboard = () => {
const mountRef = useRef(null); // Указатель монтирования
const sceneRef = useRef(null); // Указатель на сцену
const mouse = new THREE.Vector2(); // Мышка
const {websocketStruct, sendMessage} = useWebsocketConnection(1, 1);
const {websocketStruct, sendMessage} = useWebsocketConnection(getCustomCookie("userToken"), 1);
const [heightData, setHeightData] = useState(RandomHeightPoint()); // Высоты
const [formData, setFormData] = useState({ x: '', y: '', height: '' }); // Форма для ввода данных карты
const [mouseState, setMouseState] = useState({ x: 0, y: 0, z: 0 }); // Форма для ввода данных карты
@ -38,6 +37,45 @@ const Dashboard = () => {
baseStationField1: "",
baseStationField2: ""
});
const [searchTerm, setSearchTerm] = useState("");
const handleSearch = () => {
const foundDrone = drones.find(drone => drone.name.toLowerCase() === searchTerm.toLowerCase());
const foundBaseStation = baseStation.find(base => base.name.toLowerCase() === searchTerm.toLowerCase());
if (foundDrone) {
// Выделяем найденный дрон
if (selectedDrone) {
selectedDrone.getObject().traverse((child) => {
if (child.isMesh) {
child.material.color.set(0xff0000);
}
});
}
foundDrone.getObject().traverse((child) => {
if (child.isMesh) {
child.material.color.set(0x00ff00);
}
});
setSelectedDrone(foundDrone);
} else if (foundBaseStation) {
// Выделяем найденную базу
if (selectedBaseStation) {
selectedBaseStation.getObject().traverse((child) => {
if (child.isMesh) {
child.material.color.set(0x0000ff);
}
});
}
foundBaseStation.getObject().traverse((child) => {
if (child.isMesh) {
child.material.color.set(0x00ff00);
}
});
setSelectedBaseStation(foundBaseStation);
} else {
alert("Объект с указанным именем не найден");
}
};
const handleTypeChange = (type) => {
@ -45,14 +83,8 @@ const Dashboard = () => {
setFormData({ ...formData, type });
};
const handleSendMessage = () => {
const message = {
signal: 0,
data: {
key: 'value'
}
};
sendMessage(message);
const handleDeleteSignal = (name) => {
sendMessage(DeleteSignal(name));
};
@ -82,11 +114,40 @@ const Dashboard = () => {
isModalSearchOpen: false,
});
};
useEffect(() => {
if (websocketStruct) {
console.log('Received WebSocket Data:', websocketStruct);
if (websocketStruct && websocketStruct.modulation) {
const modulation = websocketStruct.modulation;
// Парсинг карты
const map = modulation.map;
setMapSettings({
name: map.name,
minBound: map.map.MinBound,
maxBound: map.map.MaxBound,
heightData: map.map.HeightData,
maxHeight: 20,
});
setHeightData(map.map.HeightData.flatMap((row, x) => row.map((height, y) => new HeightPoint(x, y, height))));
// Парсинг объектов
const parsedDrones = [];
const parsedBaseStations = [];
modulation.objects.forEach((obj) => {
if (obj.type === 1) {
const drone = new Drone(obj.name);
drone.setPosition(...obj.coords);
parsedDrones.push(drone);
} else if (obj.type === 2) {
const base = new BaseStation(obj.name);
base.setPosition(...obj.coords);
parsedBaseStations.push(base);
}
});
setDrones(parsedDrones);
setBaseStation(parsedBaseStations);
}
}, [websocketStruct]);
useEffect(() => {
if (!mountRef.current) return;
@ -94,7 +155,7 @@ const Dashboard = () => {
let height = mountRef.current.clientHeight;
// Создаем объект сцены
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
// Сохраняем ссылку на сцену
sceneRef.current = scene;
// Создаем камеру
@ -140,7 +201,7 @@ const Dashboard = () => {
mesh.rotation.x = -Math.PI / 2;
scene.add(mesh);
const axesHelper = new THREE.AxesHelper(10);
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);
const controls = new OrbitControls(camera, renderer.domElement);
@ -149,35 +210,69 @@ const Dashboard = () => {
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 50, 50).normalize();
scene.add(light);
// Обработка кликов на сцене
const handleClick = (event) => {
// Обработка кликов на сцене
const handleClick = (event) => {
// Рассчитываем координаты мыши в нормализованной системе координат и округляем до целого числа
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
console.log('Mouse coordinates:', mouse);
// Создаем объект Raycaster для проверки пересечений
const raycaster = new THREE.Raycaster();
const rayHelper = new THREE.ArrowHelper(raycaster.ray.direction, raycaster.ray.origin, 100, 0xff4444);
scene.add(rayHelper);
raycaster.setFromCamera(mouse, camera);
// Проверяем пересечения с объектами дронов
const intersects = raycaster.intersectObjects(drones.map(drone => drone.getObject()), true);
console.log('Intersections:', intersects)
console.log('Drones:', drones.map(drone => [drone.getObject(), drone.getObject().position, drone.name]))
console.log('Intersections:', intersects);
console.log('Drones:', drones.map(drone => [drone.getObject(), drone.getObject().position, drone.name]));
if (intersects.length > 0) {
const selected = intersects[0].object;
console.log('Clicked on:', selected); // Лог для проверки объекта
const drone = drones.find(drone => drone.getObject().children[0] === selected);
// Поиск дрона рекурсивно
const drone = drones.find(drone => containsObjectRecursively(drone.getObject(), selected));
if (drone) {
// Сбрасываем цвет предыдущего выбранного дрона
if (selectedDrone) {
selectedDrone.getObject().children[0].material.color.set(0xff0000);
drone.getObject().visible = true;
// selectedDrone.getObject().material.color.set(0xff0000);
}
drone.getObject().children[0].material.color.set(0xff1111);
// Устанавливаем цвет выбранного дрона
// drone.getObject().material.color.set(0xff1111);
drone.getObject().visible = false;
console.log(drone);
setSelectedDrone(drone);
}
} else {
// Сбрасываем выбор, если ни один дрон не был выбран
if (selectedDrone) {
selectedDrone.getObject().children[0].material.color.set(0xff0000);
// selectedDrone.getObject().material.color.set(0xff0000);
selectedDrone .getObject().visible = true;
setSelectedDrone(null);
}
}
};
};
// Рекурсивная функция для поиска объекта среди детей
const containsObjectRecursively = (parent, target) => {
if (parent === target) {
return true;
}
for (let child of parent.children) {
if (containsObjectRecursively(child, target)) {
return true;
}
}
return false;
};
const handleMouseMove = (event) => {
let x = event.x;
@ -226,21 +321,36 @@ const Dashboard = () => {
// Создаем новый материал для каждого дрона
drone.setPosition(Math.random() * 20 - 10, 20, Math.random() * 20 - 10);
setDrones((prevDrones) => [...prevDrones, drone]);
console.log(drones.map(drone => drone.getObject().children[0]));
console.log(drones.map(drone => drone.getObject()));
let json_signal = spawnJsonSignal(1, current / 1000 + "", "" + Math.round(drone.getObject().position.x) + "," + Math.round(drone.getObject().position.y) + "," + Math.round(drone.getObject().position.z) + "");
sendMessage(json_signal)
sceneRef.current.add(drone.getObject());
}
};
// Добавление дрона в сцену
const handleAddBaseStation = () => {
// Добавление базовой станции в сцену
const handleAddBaseStation = () => {
if (sceneRef.current) {
const current = Date.now();
const base = new BaseStation(current / 1000);
base.setPosition(Math.random() * 20 - 10, 20, Math.random() * 20 - 10);
// Выбираем случайные координаты X и Z
const x = Math.random() * 20 - 10;
const z = Math.random() * 20 - 10;
// Находим точку высоты для установки базовой станции на карту
const heightPoint = heightData.find(point => point.x === Math.round(x) && point.y === Math.round(z));
console.log(heightPoint)
const y = heightPoint ? heightPoint.height : 0;
// Устанавливаем позицию базовой станции на основании высоты карты
base.setPosition(x, y, z);
setBaseStation((prev) => [...prev, base]);
console.log(baseStation.map(b => b.getObject().children[0]));
sceneRef.current.add(base.getObject());
}
};
};
// Обработчик ввода на форму высот
const handleInputChange = (event) => {
@ -449,15 +559,20 @@ const Dashboard = () => {
<ModalWindow isOpen={modals.isModalSearchOpen} onClose={closeAllModals}>
<h2>Поиск</h2>
<input type="text" placeholder="Введите имя" />
<button onClick={() => alert('Добавлено')}>Выбор</button>
<input
type="text"
placeholder="Введите имя"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button onClick={handleSearch}>Найти</button>
</ModalWindow>
<ModalWindow isOpen={modals.isModalDeleteOpen} onClose={closeAllModals}>
<h2>Удаление</h2>
<h2>Вы точно хотите удалить объект X?</h2>
<button onClick={closeAllModals}>Отмена</button>
<button onClick={() => handleSendMessage() }>Удалить</button>
<button onClick={() => handleDeleteSignal(1) }>Удалить</button>
</ModalWindow>
<ModalWindow isOpen={modals.isModalOpen} onClose={closeAllModals}>

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { setCustomCookie, getCustomCookie } from '../Services/Local'
import { giveMeServerAddress } from '../Services/Server'
import '../Login.scss';
@ -13,16 +14,37 @@ const Login = ({ onLogin }) => {
onLogin();
}
}, [])
const handleSubmit = (e) => {
const handleSubmit = async (e) => {
e.preventDefault();
// Simple login/password check
if (username === 'admin' && password === 'admin') {
// TODO :: CHANGE
setCustomCookie('userToken', '12345', { expires: 7 }); // Куки с истечением через 7 дней
onLogin(); // Call function on successful login
} else {
setError('Неверный логин или пароль');
try {
// Отправляем запрос на сервер
const response = await fetch(giveMeServerAddress() + "/auth/"+ username +"/" + password, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
// Проверяем статус ответа
if (!response.ok) {
// Если статус не 200, ничего не делаем
setError('Ошибка авторизации. Пожалуйста, попробуйте снова.');
return;
}
// Парсим тело ответа в формате JSON
const data = await response.json();
// Устанавливаем userToken из тела ответа
setCustomCookie('userToken', data.userid, { expires: 7 });
// Вызываем функцию после успешного входа в систему
onLogin();
} catch (error) {
// Обработка ошибок сети
setError('Произошла ошибка. Пожалуйста, попробуйте снова позже.');
}
};

View File

@ -8,7 +8,6 @@ const Main = () => {
useEffect(() => {
getSimulations();
setCustomCookie('userToken', '12345', { expires: 7 }); // Куки с истечением через 7 дней
}, []);

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import '../UserAccount.scss';
import {getCustomCookie} from '../Services/Local';
const UserAccount = () => {
const [userData, setUserData] = useState({
username: 'Никита Николаевич',
@ -10,6 +10,15 @@ const UserAccount = () => {
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState(userData);
const [debug, setDebug] = useState(false)
const enableDebug = () => {
if (debug){
setDebug(false)
} else {
setDebug(true)
}
};
const handleChange = (e) => {
const { name, value } = e.target;
@ -77,6 +86,18 @@ const UserAccount = () => {
</button>
</div>
)}
<button className="btn btn-info" onClick={() => enableDebug()}>
Turn Debug Mode
</button>
{debug ? <p>
debug:
UserToken: {getCustomCookie("userToken")}
</p>
:
<p></p>
}
</div>
);
};

View File

@ -15,6 +15,12 @@ class Drone {
}, undefined, (error) => {
console.error("Ошибка загрузки GLTF:", error);
});
// Создаем красный куб вместо GLB модели
// const geometry = new THREE.BoxGeometry(2, 2, 2);
// const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
// const cube = new THREE.Mesh(geometry, material);
// cube.scale.set(scale, scale, scale);
// this.object.add(cube);
}
setPosition(x, y, z) {

View File

@ -86,6 +86,7 @@ func main() {
if err != nil {
panic(err)
}
database.Instance = database.NewDbConnection()
err = server.SpawnServer()
if err != nil {
panic(err)

View File

@ -1,6 +1,7 @@
package websocket
import (
"fmt"
"moxitech/dns/package/math/simulator"
"moxitech/dns/package/utils/randomizer"
"time"
@ -37,8 +38,15 @@ func (o *Modulation) DeleteObjectIfExists(name string) {
}
}
}
func (o *Modulation) MirrorPayloadToSimulationStruct() {
// MirrorPayloadToSimulationStruct - вернет фулл копию симуляции
func (o *Modulation) MirrorPayloadToSimulationStruct() Modulation {
shadow_sim_copy := Modulation{
Map: o.Map,
Objects: o.Objects,
Ts_update: o.Ts_update,
}
return shadow_sim_copy
}
func (o *Modulation) RunSimulator() {
@ -59,6 +67,8 @@ func (o *Modulation) RunSimulator() {
// HeightData: heightData,
// }
//
// ? mapObj := o.Map
// // Определяем дронов
//
// drones := []*simulator.Drone{
@ -128,6 +138,7 @@ func (o *Modulation) AddObject(name string, obj_type int, coords [3]int, params
// Значит объект не станция или не дрон
return
}
fmt.Println(name, obj_type, coords, params)
if o.Objects == nil {
o.Objects = make([]*SystemObject, 0)
}

View File

@ -18,12 +18,12 @@ type MongoDbInstance struct {
Db *mongo.Database
}
var instance *MongoDbInstance
var Instance *MongoDbInstance
var once sync.Once
// giveMeMongoConnectionString возвращает строку подключения к MongoDB
func giveMeMongoConnectionString() string {
return "mongodb://moxitech:moxitech@localhost:27017/" // Замените строку на свою строку подключения
return "mongodb://moxitech:moxitech@mongo:27017/" // Замените строку на свою строку подключения
}
// NewDbConnection создает подключение к базе данных MongoDB и возвращает единственный экземпляр MongoDbInstance
@ -49,13 +49,13 @@ func NewDbConnection() *MongoDbInstance {
log.Println("Успешное подключение к MongoDB")
instance = &MongoDbInstance{
Instance = &MongoDbInstance{
Client: client,
Db: client.Database(os.Getenv("MONGO_INITDB_DATABASE")),
}
})
return instance
return Instance
}
// InsertIntoSimulations вставляет данные в коллекцию "simulations"

View File

@ -5,24 +5,42 @@ import (
"moxitech/dns/internal/database"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
// GET localhost:8080/auth/username?/password?
// POST localhost:8080/auth/username?/password?
func AuthUser(c *fiber.Ctx) error {
username := c.Params("username")
password := c.Params("password")
if username == "" || password == "" {
return c.Status(501).SendString("username and password has required!")
return c.Status(403).JSON(fiber.Map{
"error": "username and password are required!",
})
}
user, err := database.DbDriver.GetUserByName(username)
if err == gorm.ErrRecordNotFound {
return c.Status(404).JSON(fiber.Map{
"error": "User not found",
})
}
if err != nil {
return c.Status(501).SendString(err.Error())
return c.Status(501).JSON(fiber.Map{
"error": err.Error(),
})
}
if user == nil {
return c.Status(403).SendString(fmt.Sprintf("User with username: %v not found", username))
return c.Status(403).JSON(fiber.Map{
"error": fmt.Sprintf("User with username: %v not found", username),
})
}
if user.Password != password {
return c.Status(403).SendString("Bad password!")
return c.Status(403).JSON(fiber.Map{
"error": "Bad password!",
})
}
return c.Status(200).SendString(fmt.Sprintf("Welcome to Drone Network Simulator server-side API! \n UserId: %v ", user.ID))
return c.Status(200).JSON(fiber.Map{
"userid": user.ID,
})
}

View File

@ -111,6 +111,7 @@ func (room *WebsocketRoom) InsertObject(message websocket.SignalMessage) error {
return fmt.Errorf("[insert] error parsing data: %v", err)
}
fmt.Println("st 1")
// Получаем значения name и params
name := parsedData.Name
params := parsedData.Params
@ -119,25 +120,31 @@ func (room *WebsocketRoom) InsertObject(message websocket.SignalMessage) error {
if params["coords"] == "" {
return fmt.Errorf("[insert] coords is empty")
}
fmt.Println("st 2")
// Разделяем строку координат на отдельные элементы
coordinateStrings := strings.Split(params["coords"], ",")
// Инициализируем массив для хранения координат
coordinates := make([]int, len(coordinateStrings))
fmt.Println("st 3")
// Преобразуем каждую координату в целое число
for i, coord := range coordinateStrings {
parsedCoord, err := strconv.Atoi(strings.TrimSpace(coord))
if err != nil {
fmt.Println(fmt.Errorf("[insert] invalid coordinate: %s", coord))
return fmt.Errorf("[insert] invalid coordinate: %s", coord)
}
coordinates[i] = parsedCoord
}
fmt.Println("st 4")
params["coords"] = ""
// Добавляем объект в комнату
roomsMutex.Lock()
defer roomsMutex.Unlock()
fmt.Println("st 5")
room.Modulation.AddObject(name, type_obj, [3]int{coordinates[0], coordinates[1], coordinates[2]}, params)
return nil
@ -199,6 +206,13 @@ func (room *WebsocketRoom) InsertExampleData() {
room.Modulation.SpawnExampleData()
}
// InsertBasicData вставляет базовые случайные данные в симуляцию
func (room *WebsocketRoom) StoreMongo() {
roomsMutex.Lock()
defer roomsMutex.Unlock()
room.Modulation.MirrorPayloadToSimulationStruct()
}
// InsertMapFromJson вставляет высоты и прочие данные карты в симуляцию, по шаблону
func (room *WebsocketRoom) InsertMapFromJson(template string) {
// TODO
@ -273,6 +287,8 @@ func (room *WebsocketRoom) WebsocketAction(message []byte) (error, bool) {
case 33:
// SYNC SIGNAL
case 100:
case 101:
// room.StoreMongo(Signal)
case 301: