From 8c2c70cda5ee2952f62628feb1b12d93a383c40a Mon Sep 17 00:00:00 2001 From: Kota Kakihara Date: Tue, 5 Jul 2022 13:32:53 +0900 Subject: [PATCH 1/4] Added Initial Files --- README.md | 52 + aspnetapp/Dockerfile | 27 + aspnetapp/WINGS/.gitignore | 242 + aspnetapp/WINGS/ClientApp/.gitignore | 21 + aspnetapp/WINGS/ClientApp/README.md | 2228 ++ aspnetapp/WINGS/ClientApp/package-lock.json | 18234 ++++++++++++++++ aspnetapp/WINGS/ClientApp/package.json | 75 + aspnetapp/WINGS/ClientApp/public/favicon.ico | Bin 0 -> 32038 bytes aspnetapp/WINGS/ClientApp/public/index.html | 41 + .../WINGS/ClientApp/public/manifest.json | 15 + aspnetapp/WINGS/ClientApp/src/App.tsx | 58 + aspnetapp/WINGS/ClientApp/src/Router.tsx | 25 + .../WINGS/ClientApp/src/assets/img/logo.png | Bin 0 -> 61299 bytes .../WINGS/ClientApp/src/assets/style.css | 187 + aspnetapp/WINGS/ClientApp/src/assets/theme.ts | 46 + .../src/components/command/CommandLog.tsx | 14 + .../src/components/command/CommandSender.tsx | 16 + .../command/log_display/CmdLogDisplayArea.tsx | 53 + .../command/log_display/CmdLogTabPanel.tsx | 63 + .../command/log_display/CmdLogTableRow.tsx | 110 + .../command/plan_display/OpenPlanDialog.tsx | 103 + .../command/plan_display/PlanDisplayArea.tsx | 147 + .../command/plan_display/PlanTabPanel.tsx | 328 + .../command/plan_display/RequestTableRow.tsx | 153 + .../command/plan_edit/CommandSelectArea.tsx | 149 + .../command/plan_edit/SetParamTable.tsx | 125 + .../src/components/common/CheckBox.tsx | 20 + .../components/common/ConfirmationDialog.tsx | 53 + .../common/EditableInputTableCell.tsx | 43 + .../common/EditableSelectTableCell.tsx | 30 + .../src/components/common/ErrorDialog.tsx | 55 + .../components/common/FileTreeMultiView.tsx | 200 + .../src/components/common/FileTreeView.tsx | 174 + .../components/common/IconButtonInTabs.tsx | 14 + .../src/components/common/LoadingBackDrop.tsx | 32 + .../src/components/common/RadioBox.tsx | 32 + .../src/components/common/SelectBox.tsx | 43 + .../src/components/common/TransferList.tsx | 179 + .../src/components/compo/ComponentList.tsx | 235 + .../src/components/compo/ComponentManage.tsx | 31 + .../src/components/header/DrawerMenus.tsx | 81 + .../src/components/header/Header.tsx | 46 + .../src/components/header/HeaderMenus.tsx | 67 + .../src/components/history/HistoryDetail.tsx | 68 + .../src/components/history/LogExportArea.tsx | 152 + .../components/history/OperationHistory.tsx | 174 + .../ClientApp/src/components/home/Home.tsx | 51 + .../src/components/home/OperationList.tsx | 136 + .../components/home/StartOperationArea.tsx | 147 + .../components/telemetry/TelemetryViewer.tsx | 71 + .../telemetry/view_display/GraphTabPanel.tsx | 305 + .../view_display/OpenGraphTabDialog.tsx | 125 + .../view_display/OpenLayoutDialog.tsx | 257 + .../view_display/OpenPacketTabDialog.tsx | 126 + .../telemetry/view_display/OpenViewDialog.tsx | 144 + .../telemetry/view_display/PacketTabPanel.tsx | 194 + .../view_display/ViewDisplayBlock.tsx | 142 + .../telemetry/view_display/ViewTabPanel.tsx | 56 + .../WINGS/ClientApp/src/constants/index.ts | 3 + aspnetapp/WINGS/ClientApp/src/index.tsx | 23 + .../WINGS/ClientApp/src/models/Command.ts | 35 + .../WINGS/ClientApp/src/models/CommandPlan.ts | 43 + .../WINGS/ClientApp/src/models/Component.ts | 7 + .../ClientApp/src/models/ErrorDialogState.ts | 4 + .../WINGS/ClientApp/src/models/FileIndex.ts | 5 + .../ClientApp/src/models/LocationState.ts | 3 + .../WINGS/ClientApp/src/models/Operation.ts | 22 + .../ClientApp/src/models/PaginationMeta.ts | 12 + .../WINGS/ClientApp/src/models/Telemetry.ts | 59 + .../ClientApp/src/models/TelemetryView.ts | 35 + .../WINGS/ClientApp/src/models/UIState.ts | 6 + aspnetapp/WINGS/ClientApp/src/models/index.ts | 11 + .../WINGS/ClientApp/src/react-app-env.d.ts | 1 + .../ClientApp/src/redux/commands/actions.ts | 18 + .../ClientApp/src/redux/commands/reducers.ts | 37 + .../ClientApp/src/redux/commands/selectors.ts | 24 + .../ClientApp/src/redux/operations/actions.ts | 17 + .../src/redux/operations/operations.ts | 74 + .../src/redux/operations/reducers.ts | 20 + .../src/redux/operations/selectors.ts | 14 + .../ClientApp/src/redux/plans/actions.ts | 106 + .../ClientApp/src/redux/plans/operations.ts | 59 + .../ClientApp/src/redux/plans/reducers.ts | 228 + .../ClientApp/src/redux/plans/selectors.ts | 46 + .../ClientApp/src/redux/store/RootState.ts | 10 + .../ClientApp/src/redux/store/initialState.ts | 102 + .../WINGS/ClientApp/src/redux/store/store.ts | 41 + .../src/redux/telemetries/actions.ts | 26 + .../src/redux/telemetries/reducers.ts | 56 + .../src/redux/telemetries/selectors.ts | 14 + .../WINGS/ClientApp/src/redux/ui/actions.ts | 29 + .../WINGS/ClientApp/src/redux/ui/reducers.ts | 46 + .../WINGS/ClientApp/src/redux/ui/selectors.ts | 14 + .../ClientApp/src/redux/views/actions.ts | 111 + .../ClientApp/src/redux/views/reducers.ts | 217 + .../ClientApp/src/redux/views/selectors.ts | 34 + aspnetapp/WINGS/ClientApp/tsconfig.json | 26 + .../WINGS/Controllers/CommandController.cs | 160 + .../WINGS/Controllers/ComponentController.cs | 97 + .../WINGS/Controllers/HistoryController.cs | 193 + .../WINGS/Controllers/LayoutController.cs | 65 + .../WINGS/Controllers/OperationController.cs | 104 + .../WINGS/Controllers/TelemetryController.cs | 51 + aspnetapp/WINGS/Data/ApplicationDbContext.cs | 35 + aspnetapp/WINGS/Data/CommandDbRepository.cs | 128 + .../WINGS/Data/CommandFileLogRepository.cs | 164 + aspnetapp/WINGS/Data/CommandFileRepository.cs | 587 + .../Interfaces/ICommandFileLogRepository.cs | 15 + .../Data/Interfaces/ICommandFileRepository.cs | 12 + .../WINGS/Data/Interfaces/IDbRepository.cs | 11 + .../Data/Interfaces/ILayoutRepository.cs | 14 + .../Interfaces/ITelemetryLogRepository.cs | 18 + aspnetapp/WINGS/Data/LayoutRepository.cs | 114 + aspnetapp/WINGS/Data/TelemetryDbRepository.cs | 230 + .../WINGS/Data/TelemetryLogRepository.cs | 235 + aspnetapp/WINGS/Library/CRC.cs | 191 + aspnetapp/WINGS/Library/Paginator.cs | 39 + aspnetapp/WINGS/Library/TextFieldParser.cs | 520 + aspnetapp/WINGS/Library/Zipper.cs | 28 + aspnetapp/WINGS/Logs/.gitignore | 2 + .../ApplicationDbContextModelSnapshot.cs | 151 + aspnetapp/WINGS/Models/Command.cs | 63 + aspnetapp/WINGS/Models/CommandFile.cs | 41 + aspnetapp/WINGS/Models/CommandLog.cs | 40 + aspnetapp/WINGS/Models/Component.cs | 26 + aspnetapp/WINGS/Models/Exception.cs | 94 + aspnetapp/WINGS/Models/Layout.cs | 38 + aspnetapp/WINGS/Models/Operation.cs | 41 + aspnetapp/WINGS/Models/Pagination.cs | 27 + aspnetapp/WINGS/Models/Telemetry.cs | 67 + aspnetapp/WINGS/Models/TlmCmdFileConfig.cs | 23 + aspnetapp/WINGS/Models/TmtcPacket.cs | 13 + aspnetapp/WINGS/Models/ZipItem.cs | 10 + aspnetapp/WINGS/Pages/Error.cshtml | 26 + aspnetapp/WINGS/Pages/Error.cshtml.cs | 31 + aspnetapp/WINGS/Pages/_ViewImports.cshtml | 3 + aspnetapp/WINGS/Program.cs | 50 + .../WINGS/Properties/launchSettings.json | 27 + aspnetapp/WINGS/Protos/tmtc.proto | 29 + .../WINGS/Services/Core/CommandService.cs | 420 + .../Core/Interfaces/ICommandService.cs | 25 + .../Core/Interfaces/ILayoutService.cs | 17 + .../Core/Interfaces/IOperationService.cs | 18 + .../Core/Interfaces/ITelemetryService.cs | 18 + .../Interfaces/ITlmCmdFileConfigBuilder.cs | 10 + .../WINGS/Services/Core/LayoutService.cs | 122 + .../WINGS/Services/Core/OperationService.cs | 227 + .../WINGS/Services/Core/TelemetryService.cs | 87 + .../Services/Core/TlmCmdFileConfigBuilder.cs | 54 + .../Manager/Interfaces/ITcPacketManager.cs | 13 + .../Manager/Interfaces/ITmPacketManager.cs | 15 + .../Manager/Interfaces/ITmtcHandlerFactory.cs | 13 + .../Services/TMTC/Manager/TcPacketManager.cs | 42 + .../Services/TMTC/Manager/TmPacketManager.cs | 127 + .../TMTC/Manager/TmtcHandlerFactory.cs | 89 + .../Abstracts/TcPacketGeneratorBase.cs | 221 + .../Abstracts/TmPacketAnalyzerBase.cs | 312 + .../Interfaces/ITcPacketGenerator.cs | 9 + .../Processor/Interfaces/ITmPacketAnalyzer.cs | 13 + .../IsslCommonTcPacketGenerator.cs | 71 + .../ISSL_COMMON/IsslCommonTmPacketAnalyzer.cs | 29 + .../UserDefined/MOBC/MobcTcPacketGenerator.cs | 427 + .../UserDefined/MOBC/MobcTmPacketAnalyzer.cs | 297 + .../Interfaces/ITmtcPacketService.cs | 9 + .../TMTC/Transferer/TmtcIf/ITcPacketQueue.cs | 13 + .../TMTC/Transferer/TmtcIf/TcPacketQueue.cs | 54 + .../Transferer/TmtcIf/TmtcPacketService.cs | 100 + aspnetapp/WINGS/Startup.cs | 123 + .../WINGS/TlmCmd/ISSL_COMMON/cmddb/CMD_DB.csv | 6 + .../WINGS/TlmCmd/ISSL_COMMON/tlmdb/HK.csv | 9 + aspnetapp/WINGS/TlmCmd/OBC/cmddb/.gitkeep | 0 .../WINGS/TlmCmd/OBC/cmdplan/main/.gitkeep | 0 .../WINGS/TlmCmd/OBC/cmdplan/sub/.gitkeep | 0 .../WINGS/TlmCmd/OBC/cmdplan/test/.gitkeep | 0 aspnetapp/WINGS/TlmCmd/OBC/tlmdb/.gitkeep | 0 .../WINGS/TlmCmd/Sample/cmddb/CMD_DB.csv | 6 + .../WINGS/TlmCmd/Sample/cmdplan/sample.ops | 54 + aspnetapp/WINGS/TlmCmd/Sample/tlmdb/HK.csv | 10 + aspnetapp/WINGS/WINGS.csproj | 64 + aspnetapp/WINGS/appsettings.Development.json | 13 + aspnetapp/WINGS/appsettings.Production.json | 25 + aspnetapp/WINGS/certificate.pfx | Bin 0 -> 2638 bytes docker-compose.yml | 31 + mysql/Dockerfile | 23 + mysql/data/.gitignore | 2 + mysql/init/init.sql | 55 + 186 files changed, 34719 insertions(+) create mode 100644 README.md create mode 100644 aspnetapp/Dockerfile create mode 100644 aspnetapp/WINGS/.gitignore create mode 100644 aspnetapp/WINGS/ClientApp/.gitignore create mode 100644 aspnetapp/WINGS/ClientApp/README.md create mode 100644 aspnetapp/WINGS/ClientApp/package-lock.json create mode 100644 aspnetapp/WINGS/ClientApp/package.json create mode 100644 aspnetapp/WINGS/ClientApp/public/favicon.ico create mode 100644 aspnetapp/WINGS/ClientApp/public/index.html create mode 100644 aspnetapp/WINGS/ClientApp/public/manifest.json create mode 100644 aspnetapp/WINGS/ClientApp/src/App.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/Router.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/assets/img/logo.png create mode 100644 aspnetapp/WINGS/ClientApp/src/assets/style.css create mode 100644 aspnetapp/WINGS/ClientApp/src/assets/theme.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/CommandLog.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/CommandSender.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/log_display/CmdLogDisplayArea.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/log_display/CmdLogTabPanel.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/log_display/CmdLogTableRow.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/plan_display/OpenPlanDialog.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/plan_display/PlanDisplayArea.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/plan_display/PlanTabPanel.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/plan_display/RequestTableRow.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/plan_edit/CommandSelectArea.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/command/plan_edit/SetParamTable.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/CheckBox.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/ConfirmationDialog.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/EditableInputTableCell.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/EditableSelectTableCell.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/ErrorDialog.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/FileTreeMultiView.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/FileTreeView.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/IconButtonInTabs.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/LoadingBackDrop.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/RadioBox.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/SelectBox.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/common/TransferList.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/compo/ComponentList.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/compo/ComponentManage.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/header/DrawerMenus.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/header/Header.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/header/HeaderMenus.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/history/HistoryDetail.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/history/LogExportArea.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/history/OperationHistory.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/home/Home.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/home/OperationList.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/home/StartOperationArea.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/telemetry/TelemetryViewer.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/GraphTabPanel.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenGraphTabDialog.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenLayoutDialog.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenPacketTabDialog.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenViewDialog.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/PacketTabPanel.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/ViewDisplayBlock.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/ViewTabPanel.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/constants/index.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/index.tsx create mode 100644 aspnetapp/WINGS/ClientApp/src/models/Command.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/CommandPlan.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/Component.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/ErrorDialogState.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/FileIndex.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/LocationState.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/Operation.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/PaginationMeta.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/Telemetry.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/TelemetryView.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/UIState.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/models/index.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/react-app-env.d.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/commands/actions.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/commands/reducers.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/commands/selectors.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/operations/actions.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/operations/operations.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/operations/reducers.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/operations/selectors.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/plans/actions.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/plans/operations.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/plans/reducers.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/plans/selectors.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/store/RootState.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/store/initialState.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/store/store.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/telemetries/actions.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/telemetries/reducers.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/telemetries/selectors.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/ui/actions.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/ui/reducers.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/ui/selectors.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/views/actions.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/views/reducers.ts create mode 100644 aspnetapp/WINGS/ClientApp/src/redux/views/selectors.ts create mode 100644 aspnetapp/WINGS/ClientApp/tsconfig.json create mode 100644 aspnetapp/WINGS/Controllers/CommandController.cs create mode 100644 aspnetapp/WINGS/Controllers/ComponentController.cs create mode 100644 aspnetapp/WINGS/Controllers/HistoryController.cs create mode 100644 aspnetapp/WINGS/Controllers/LayoutController.cs create mode 100644 aspnetapp/WINGS/Controllers/OperationController.cs create mode 100644 aspnetapp/WINGS/Controllers/TelemetryController.cs create mode 100644 aspnetapp/WINGS/Data/ApplicationDbContext.cs create mode 100644 aspnetapp/WINGS/Data/CommandDbRepository.cs create mode 100644 aspnetapp/WINGS/Data/CommandFileLogRepository.cs create mode 100644 aspnetapp/WINGS/Data/CommandFileRepository.cs create mode 100644 aspnetapp/WINGS/Data/Interfaces/ICommandFileLogRepository.cs create mode 100644 aspnetapp/WINGS/Data/Interfaces/ICommandFileRepository.cs create mode 100644 aspnetapp/WINGS/Data/Interfaces/IDbRepository.cs create mode 100644 aspnetapp/WINGS/Data/Interfaces/ILayoutRepository.cs create mode 100644 aspnetapp/WINGS/Data/Interfaces/ITelemetryLogRepository.cs create mode 100644 aspnetapp/WINGS/Data/LayoutRepository.cs create mode 100644 aspnetapp/WINGS/Data/TelemetryDbRepository.cs create mode 100644 aspnetapp/WINGS/Data/TelemetryLogRepository.cs create mode 100644 aspnetapp/WINGS/Library/CRC.cs create mode 100644 aspnetapp/WINGS/Library/Paginator.cs create mode 100644 aspnetapp/WINGS/Library/TextFieldParser.cs create mode 100644 aspnetapp/WINGS/Library/Zipper.cs create mode 100644 aspnetapp/WINGS/Logs/.gitignore create mode 100644 aspnetapp/WINGS/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 aspnetapp/WINGS/Models/Command.cs create mode 100644 aspnetapp/WINGS/Models/CommandFile.cs create mode 100644 aspnetapp/WINGS/Models/CommandLog.cs create mode 100644 aspnetapp/WINGS/Models/Component.cs create mode 100644 aspnetapp/WINGS/Models/Exception.cs create mode 100644 aspnetapp/WINGS/Models/Layout.cs create mode 100644 aspnetapp/WINGS/Models/Operation.cs create mode 100644 aspnetapp/WINGS/Models/Pagination.cs create mode 100644 aspnetapp/WINGS/Models/Telemetry.cs create mode 100644 aspnetapp/WINGS/Models/TlmCmdFileConfig.cs create mode 100644 aspnetapp/WINGS/Models/TmtcPacket.cs create mode 100644 aspnetapp/WINGS/Models/ZipItem.cs create mode 100644 aspnetapp/WINGS/Pages/Error.cshtml create mode 100644 aspnetapp/WINGS/Pages/Error.cshtml.cs create mode 100644 aspnetapp/WINGS/Pages/_ViewImports.cshtml create mode 100644 aspnetapp/WINGS/Program.cs create mode 100644 aspnetapp/WINGS/Properties/launchSettings.json create mode 100644 aspnetapp/WINGS/Protos/tmtc.proto create mode 100644 aspnetapp/WINGS/Services/Core/CommandService.cs create mode 100644 aspnetapp/WINGS/Services/Core/Interfaces/ICommandService.cs create mode 100644 aspnetapp/WINGS/Services/Core/Interfaces/ILayoutService.cs create mode 100644 aspnetapp/WINGS/Services/Core/Interfaces/IOperationService.cs create mode 100644 aspnetapp/WINGS/Services/Core/Interfaces/ITelemetryService.cs create mode 100644 aspnetapp/WINGS/Services/Core/Interfaces/ITlmCmdFileConfigBuilder.cs create mode 100644 aspnetapp/WINGS/Services/Core/LayoutService.cs create mode 100644 aspnetapp/WINGS/Services/Core/OperationService.cs create mode 100644 aspnetapp/WINGS/Services/Core/TelemetryService.cs create mode 100644 aspnetapp/WINGS/Services/Core/TlmCmdFileConfigBuilder.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITcPacketManager.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITmPacketManager.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITmtcHandlerFactory.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Manager/TcPacketManager.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Manager/TmPacketManager.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Manager/TmtcHandlerFactory.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Processor/Abstracts/TcPacketGeneratorBase.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Processor/Abstracts/TmPacketAnalyzerBase.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Processor/Interfaces/ITcPacketGenerator.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Processor/Interfaces/ITmPacketAnalyzer.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/ISSL_COMMON/IsslCommonTcPacketGenerator.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/ISSL_COMMON/IsslCommonTmPacketAnalyzer.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/MOBC/MobcTcPacketGenerator.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/MOBC/MobcTmPacketAnalyzer.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Transferer/Interfaces/ITmtcPacketService.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/ITcPacketQueue.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/TcPacketQueue.cs create mode 100644 aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/TmtcPacketService.cs create mode 100644 aspnetapp/WINGS/Startup.cs create mode 100644 aspnetapp/WINGS/TlmCmd/ISSL_COMMON/cmddb/CMD_DB.csv create mode 100644 aspnetapp/WINGS/TlmCmd/ISSL_COMMON/tlmdb/HK.csv create mode 100644 aspnetapp/WINGS/TlmCmd/OBC/cmddb/.gitkeep create mode 100644 aspnetapp/WINGS/TlmCmd/OBC/cmdplan/main/.gitkeep create mode 100644 aspnetapp/WINGS/TlmCmd/OBC/cmdplan/sub/.gitkeep create mode 100644 aspnetapp/WINGS/TlmCmd/OBC/cmdplan/test/.gitkeep create mode 100644 aspnetapp/WINGS/TlmCmd/OBC/tlmdb/.gitkeep create mode 100644 aspnetapp/WINGS/TlmCmd/Sample/cmddb/CMD_DB.csv create mode 100644 aspnetapp/WINGS/TlmCmd/Sample/cmdplan/sample.ops create mode 100644 aspnetapp/WINGS/TlmCmd/Sample/tlmdb/HK.csv create mode 100644 aspnetapp/WINGS/WINGS.csproj create mode 100644 aspnetapp/WINGS/appsettings.Development.json create mode 100644 aspnetapp/WINGS/appsettings.Production.json create mode 100644 aspnetapp/WINGS/certificate.pfx create mode 100644 docker-compose.yml create mode 100644 mysql/Dockerfile create mode 100644 mysql/data/.gitignore create mode 100644 mysql/init/init.sql diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d0a42e --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# WINGS +Web-based INterface Ground-station Software +WINGS is a software processing telemetry and command for satellites and satellite componetns. WINGS is a web application which can be used from both web browsers and http api requests. WINGS supports C2A-styled (https://github.com/ut-issl/c2a-core) and ISSL-styled telemetry and command formats. Users can implement other formats. +Usually, interface software is required for the connection between WINGS and satellites. WINGS_TMTC_IF is such software which supports COM port and socket connection. Users can implement other interface software. +WINGS uses ASP .NET (https://github.com/dotnet/aspnetcore) for backend software, MySQL for database, and React (https://github.com/facebook/react) for frontend software. + +## Getting Started for User +### Prerequisites +The application listed below is required: ++ [Docker](https://docs.docker.com/get-docker/) + + +### Installing +1. Open a terminal. +2. Navigate to the desired location for the repository. +3. Clone the repository. +4. Make sure that docker is running. +5. Create dokcer images in the directory containing `docker-compose.yml`. + ``` + docker-compose build + ``` + +### Running +1. Start the docker containers. + ``` + docker-compose up -d + ``` +2. Access `https://localhost:5001` from your browser. +3. If you stop the containers, you can use the following command: + ``` + docker-compose down + ``` +### Operation +1. Fulfill comment and select a component in main page. +2. Click operation start bottun. +3. Connect WINGS_TMTC_IF to the operation. +4. Click operation join bottun. +5. You can show telemetry and send command while joining operation. + +### Command +- You can send commands by clicking command line and pushing `"Shift" + "Return"` keys. +- If you want to use command files, click "+" button in command file tabs. +- If you want to add unplanned commands, select command in "Command Selection" area and click "add" button. + +### Telemetry +- You can show telemetry by cliking "+" bottun, selecting showing type and telemetry packet names, and cliking "Open" button. +- "packet" type just shows telemetry values. +- "graph" type shows telemetry graphs. +- You can save and restore telemetry showing layouts (click "Layouts" button). + +### Command Logs +- Command logs are shown in "CmdLog" page. diff --git a/aspnetapp/Dockerfile b/aspnetapp/Dockerfile new file mode 100644 index 0000000..b504f3e --- /dev/null +++ b/aspnetapp/Dockerfile @@ -0,0 +1,27 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 5001 +RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - +RUN apt-get update +RUN apt-get install -y nodejs +ENV TZ=Asia/Tokyo + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - +RUN apt-get update +RUN apt-get install -y nodejs +COPY ["WINGS/WINGS.csproj", "WINGS/"] +RUN dotnet restore "WINGS/WINGS.csproj" +COPY . . +WORKDIR "/src/WINGS" +RUN dotnet build "WINGS.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "WINGS.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +COPY ["WINGS/certificate.pfx", "./"] +ENTRYPOINT ["dotnet", "WINGS.dll"] diff --git a/aspnetapp/WINGS/.gitignore b/aspnetapp/WINGS/.gitignore new file mode 100644 index 0000000..8dc931f --- /dev/null +++ b/aspnetapp/WINGS/.gitignore @@ -0,0 +1,242 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +bin/ +out/ +Bin/ +obj/ +Obj/ + +# Visual Studio 2015 cache/options directory +.vs/ +/wwwroot/dist/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +orleans.codegen.cs + +# Ceriticate of identity server for production build +!certificate.pfx + +/node_modules + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# SQLite files +*.db + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ + +# macOS +*.DS_Store diff --git a/aspnetapp/WINGS/ClientApp/.gitignore b/aspnetapp/WINGS/ClientApp/.gitignore new file mode 100644 index 0000000..d30f40e --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/aspnetapp/WINGS/ClientApp/README.md b/aspnetapp/WINGS/ClientApp/README.md new file mode 100644 index 0000000..5a608cb --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/README.md @@ -0,0 +1,2228 @@ +This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). + +Below you will find some information on how to perform common tasks.
+You can find the most recent version of this guide [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md). + +## Table of Contents + +- [Updating to New Releases](#updating-to-new-releases) +- [Sending Feedback](#sending-feedback) +- [Folder Structure](#folder-structure) +- [Available Scripts](#available-scripts) + - [npm start](#npm-start) + - [npm test](#npm-test) + - [npm run build](#npm-run-build) + - [npm run eject](#npm-run-eject) +- [Supported Language Features and Polyfills](#supported-language-features-and-polyfills) +- [Syntax Highlighting in the Editor](#syntax-highlighting-in-the-editor) +- [Displaying Lint Output in the Editor](#displaying-lint-output-in-the-editor) +- [Debugging in the Editor](#debugging-in-the-editor) +- [Formatting Code Automatically](#formatting-code-automatically) +- [Changing the Page ``](#changing-the-page-title) +- [Installing a Dependency](#installing-a-dependency) +- [Importing a Component](#importing-a-component) +- [Code Splitting](#code-splitting) +- [Adding a Stylesheet](#adding-a-stylesheet) +- [Post-Processing CSS](#post-processing-css) +- [Adding a CSS Preprocessor (Sass, Less etc.)](#adding-a-css-preprocessor-sass-less-etc) +- [Adding Images, Fonts, and Files](#adding-images-fonts-and-files) +- [Using the `public` Folder](#using-the-public-folder) + - [Changing the HTML](#changing-the-html) + - [Adding Assets Outside of the Module System](#adding-assets-outside-of-the-module-system) + - [When to Use the `public` Folder](#when-to-use-the-public-folder) +- [Using Global Variables](#using-global-variables) +- [Adding Bootstrap](#adding-bootstrap) + - [Using a Custom Theme](#using-a-custom-theme) +- [Adding Flow](#adding-flow) +- [Adding Custom Environment Variables](#adding-custom-environment-variables) + - [Referencing Environment Variables in the HTML](#referencing-environment-variables-in-the-html) + - [Adding Temporary Environment Variables In Your Shell](#adding-temporary-environment-variables-in-your-shell) + - [Adding Development Environment Variables In `.env`](#adding-development-environment-variables-in-env) +- [Can I Use Decorators?](#can-i-use-decorators) +- [Integrating with an API Backend](#integrating-with-an-api-backend) + - [Node](#node) + - [Ruby on Rails](#ruby-on-rails) +- [Proxying API Requests in Development](#proxying-api-requests-in-development) + - ["Invalid Host Header" Errors After Configuring Proxy](#invalid-host-header-errors-after-configuring-proxy) + - [Configuring the Proxy Manually](#configuring-the-proxy-manually) + - [Configuring a WebSocket Proxy](#configuring-a-websocket-proxy) +- [Using HTTPS in Development](#using-https-in-development) +- [Generating Dynamic `<meta>` Tags on the Server](#generating-dynamic-meta-tags-on-the-server) +- [Pre-Rendering into Static HTML Files](#pre-rendering-into-static-html-files) +- [Injecting Data from the Server into the Page](#injecting-data-from-the-server-into-the-page) +- [Running Tests](#running-tests) + - [Filename Conventions](#filename-conventions) + - [Command Line Interface](#command-line-interface) + - [Version Control Integration](#version-control-integration) + - [Writing Tests](#writing-tests) + - [Testing Components](#testing-components) + - [Using Third Party Assertion Libraries](#using-third-party-assertion-libraries) + - [Initializing Test Environment](#initializing-test-environment) + - [Focusing and Excluding Tests](#focusing-and-excluding-tests) + - [Coverage Reporting](#coverage-reporting) + - [Continuous Integration](#continuous-integration) + - [Disabling jsdom](#disabling-jsdom) + - [Snapshot Testing](#snapshot-testing) + - [Editor Integration](#editor-integration) +- [Developing Components in Isolation](#developing-components-in-isolation) + - [Getting Started with Storybook](#getting-started-with-storybook) + - [Getting Started with Styleguidist](#getting-started-with-styleguidist) +- [Making a Progressive Web App](#making-a-progressive-web-app) + - [Opting Out of Caching](#opting-out-of-caching) + - [Offline-First Considerations](#offline-first-considerations) + - [Progressive Web App Metadata](#progressive-web-app-metadata) +- [Analyzing the Bundle Size](#analyzing-the-bundle-size) +- [Deployment](#deployment) + - [Static Server](#static-server) + - [Other Solutions](#other-solutions) + - [Serving Apps with Client-Side Routing](#serving-apps-with-client-side-routing) + - [Building for Relative Paths](#building-for-relative-paths) + - [Azure](#azure) + - [Firebase](#firebase) + - [GitHub Pages](#github-pages) + - [Heroku](#heroku) + - [Netlify](#netlify) + - [Now](#now) + - [S3 and CloudFront](#s3-and-cloudfront) + - [Surge](#surge) +- [Advanced Configuration](#advanced-configuration) +- [Troubleshooting](#troubleshooting) + - [`npm start` doesn’t detect changes](#npm-start-doesnt-detect-changes) + - [`npm test` hangs on macOS Sierra](#npm-test-hangs-on-macos-sierra) + - [`npm run build` exits too early](#npm-run-build-exits-too-early) + - [`npm run build` fails on Heroku](#npm-run-build-fails-on-heroku) + - [`npm run build` fails to minify](#npm-run-build-fails-to-minify) + - [Moment.js locales are missing](#momentjs-locales-are-missing) +- [Something Missing?](#something-missing) + +## Updating to New Releases + +Create React App is divided into two packages: + +* `create-react-app` is a global command-line utility that you use to create new projects. +* `react-scripts` is a development dependency in the generated projects (including this one). + +You almost never need to update `create-react-app` itself: it delegates all the setup to `react-scripts`. + +When you run `create-react-app`, it always creates the project with the latest version of `react-scripts` so you’ll get all the new features and improvements in newly created apps automatically. + +To update an existing project to a new version of `react-scripts`, [open the changelog](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md), find the version you’re currently on (check `package.json` in this folder if you’re not sure), and apply the migration instructions for the newer versions. + +In most cases bumping the `react-scripts` version in `package.json` and running `npm install` in this folder should be enough, but it’s good to consult the [changelog](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md) for potential breaking changes. + +We commit to keeping the breaking changes minimal so you can upgrade `react-scripts` painlessly. + +## Sending Feedback + +We are always open to [your feedback](https://github.com/facebookincubator/create-react-app/issues). + +## Folder Structure + +After creation, your project should look like this: + +``` +my-app/ + README.md + node_modules/ + package.json + public/ + index.html + favicon.ico + src/ + App.css + App.js + App.test.js + index.css + index.js + logo.svg +``` + +For the project to build, **these files must exist with exact filenames**: + +* `public/index.html` is the page template; +* `src/index.js` is the JavaScript entry point. + +You can delete or rename the other files. + +You may create subdirectories inside `src`. For faster rebuilds, only files inside `src` are processed by Webpack.<br> +You need to **put any JS and CSS files inside `src`**, otherwise Webpack won’t see them. + +Only files inside `public` can be used from `public/index.html`.<br> +Read instructions below for using assets from JavaScript and HTML. + +You can, however, create more top-level directories.<br> +They will not be included in the production build so you can use them for things like documentation. + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.<br> +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.<br> +You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.<br> +See the section about [running tests](#running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.<br> +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.<br> +Your app is ready to be deployed! + +See the section about [deployment](#deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Supported Language Features and Polyfills + +This project supports a superset of the latest JavaScript standard.<br> +In addition to [ES6](https://github.com/lukehoban/es6features) syntax features, it also supports: + +* [Exponentiation Operator](https://github.com/rwaldron/exponentiation-operator) (ES2016). +* [Async/await](https://github.com/tc39/ecmascript-asyncawait) (ES2017). +* [Object Rest/Spread Properties](https://github.com/sebmarkbage/ecmascript-rest-spread) (stage 3 proposal). +* [Dynamic import()](https://github.com/tc39/proposal-dynamic-import) (stage 3 proposal) +* [Class Fields and Static Properties](https://github.com/tc39/proposal-class-public-fields) (part of stage 3 proposal). +* [JSX](https://facebook.github.io/react/docs/introducing-jsx.html) and [Flow](https://flowtype.org/) syntax. + +Learn more about [different proposal stages](https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-). + +While we recommend using experimental proposals with some caution, Facebook heavily uses these features in the product code, so we intend to provide [codemods](https://medium.com/@cpojer/effective-javascript-codemods-5a6686bb46fb) if any of these proposals change in the future. + +Note that **the project only includes a few ES6 [polyfills](https://en.wikipedia.org/wiki/Polyfill)**: + +* [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) via [`object-assign`](https://github.com/sindresorhus/object-assign). +* [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) via [`promise`](https://github.com/then/promise). +* [`fetch()`](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) via [`whatwg-fetch`](https://github.com/github/fetch). + +If you use any other ES6+ features that need **runtime support** (such as `Array.from()` or `Symbol`), make sure you are including the appropriate polyfills manually, or that the browsers you are targeting already support them. + +## Syntax Highlighting in the Editor + +To configure the syntax highlighting in your favorite text editor, head to the [relevant Babel documentation page](https://babeljs.io/docs/editors) and follow the instructions. Some of the most popular editors are covered. + +## Displaying Lint Output in the Editor + +>Note: this feature is available with `react-scripts@0.2.0` and higher.<br> +>It also only works with npm 3 or higher. + +Some editors, including Sublime Text, Atom, and Visual Studio Code, provide plugins for ESLint. + +They are not required for linting. You should see the linter output right in your terminal as well as the browser console. However, if you prefer the lint results to appear right in your editor, there are some extra steps you can do. + +You would need to install an ESLint plugin for your editor first. Then, add a file called `.eslintrc` to the project root: + +```js +{ + "extends": "react-app" +} +``` + +Now your editor should report the linting warnings. + +Note that even if you edit your `.eslintrc` file further, these changes will **only affect the editor integration**. They won’t affect the terminal and in-browser lint output. This is because Create React App intentionally provides a minimal set of rules that find common mistakes. + +If you want to enforce a coding style for your project, consider using [Prettier](https://github.com/jlongster/prettier) instead of ESLint style rules. + +## Debugging in the Editor + +**This feature is currently only supported by [Visual Studio Code](https://code.visualstudio.com) and [WebStorm](https://www.jetbrains.com/webstorm/).** + +Visual Studio Code and WebStorm support debugging out of the box with Create React App. This enables you as a developer to write and debug your React code without leaving the editor, and most importantly it enables you to have a continuous development workflow, where context switching is minimal, as you don’t have to switch between tools. + +### Visual Studio Code + +You would need to have the latest version of [VS Code](https://code.visualstudio.com) and VS Code [Chrome Debugger Extension](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) installed. + +Then add the block below to your `launch.json` file and put it inside the `.vscode` folder in your app’s root directory. + +```json +{ + "version": "0.2.0", + "configurations": [{ + "name": "Chrome", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000", + "webRoot": "${workspaceRoot}/src", + "sourceMapPathOverrides": { + "webpack:///src/*": "${webRoot}/*" + } + }] +} +``` +>Note: the URL may be different if you've made adjustments via the [HOST or PORT environment variables](#advanced-configuration). + +Start your app by running `npm start`, and start debugging in VS Code by pressing `F5` or by clicking the green debug icon. You can now write code, set breakpoints, make changes to the code, and debug your newly modified code—all from your editor. + +Having problems with VS Code Debugging? Please see their [troubleshooting guide](https://github.com/Microsoft/vscode-chrome-debug/blob/master/README.md#troubleshooting). + +### WebStorm + +You would need to have [WebStorm](https://www.jetbrains.com/webstorm/) and [JetBrains IDE Support](https://chrome.google.com/webstore/detail/jetbrains-ide-support/hmhgeddbohgjknpmjagkdomcpobmllji) Chrome extension installed. + +In the WebStorm menu `Run` select `Edit Configurations...`. Then click `+` and select `JavaScript Debug`. Paste `http://localhost:3000` into the URL field and save the configuration. + +>Note: the URL may be different if you've made adjustments via the [HOST or PORT environment variables](#advanced-configuration). + +Start your app by running `npm start`, then press `^D` on macOS or `F9` on Windows and Linux or click the green debug icon to start debugging in WebStorm. + +The same way you can debug your application in IntelliJ IDEA Ultimate, PhpStorm, PyCharm Pro, and RubyMine. + +## Formatting Code Automatically + +Prettier is an opinionated code formatter with support for JavaScript, CSS and JSON. With Prettier you can format the code you write automatically to ensure a code style within your project. See the [Prettier's GitHub page](https://github.com/prettier/prettier) for more information, and look at this [page to see it in action](https://prettier.github.io/prettier/). + +To format our code whenever we make a commit in git, we need to install the following dependencies: + +```sh +npm install --save husky lint-staged prettier +``` + +Alternatively you may use `yarn`: + +```sh +yarn add husky lint-staged prettier +``` + +* `husky` makes it easy to use githooks as if they are npm scripts. +* `lint-staged` allows us to run scripts on staged files in git. See this [blog post about lint-staged to learn more about it](https://medium.com/@okonetchnikov/make-linting-great-again-f3890e1ad6b8). +* `prettier` is the JavaScript formatter we will run before commits. + +Now we can make sure every file is formatted correctly by adding a few lines to the `package.json` in the project root. + +Add the following line to `scripts` section: + +```diff + "scripts": { ++ "precommit": "lint-staged", + "start": "react-scripts start", + "build": "react-scripts build", +``` + +Next we add a 'lint-staged' field to the `package.json`, for example: + +```diff + "dependencies": { + // ... + }, ++ "lint-staged": { ++ "src/**/*.{js,jsx,json,css}": [ ++ "prettier --single-quote --write", ++ "git add" ++ ] ++ }, + "scripts": { +``` + +Now, whenever you make a commit, Prettier will format the changed files automatically. You can also run `./node_modules/.bin/prettier --single-quote --write "src/**/*.{js,jsx}"` to format your entire project for the first time. + +Next you might want to integrate Prettier in your favorite editor. Read the section on [Editor Integration](https://github.com/prettier/prettier#editor-integration) on the Prettier GitHub page. + +## Changing the Page `<title>` + +You can find the source HTML file in the `public` folder of the generated project. You may edit the `<title>` tag in it to change the title from “React App” to anything else. + +Note that normally you wouldn’t edit files in the `public` folder very often. For example, [adding a stylesheet](#adding-a-stylesheet) is done without touching the HTML. + +If you need to dynamically update the page title based on the content, you can use the browser [`document.title`](https://developer.mozilla.org/en-US/docs/Web/API/Document/title) API. For more complex scenarios when you want to change the title from React components, you can use [React Helmet](https://github.com/nfl/react-helmet), a third party library. + +If you use a custom server for your app in production and want to modify the title before it gets sent to the browser, you can follow advice in [this section](#generating-dynamic-meta-tags-on-the-server). Alternatively, you can pre-build each page as a static HTML file which then loads the JavaScript bundle, which is covered [here](#pre-rendering-into-static-html-files). + +## Installing a Dependency + +The generated project includes React and ReactDOM as dependencies. It also includes a set of scripts used by Create React App as a development dependency. You may install other dependencies (for example, React Router) with `npm`: + +```sh +npm install --save react-router +``` + +Alternatively you may use `yarn`: + +```sh +yarn add react-router +``` + +This works for any library, not just `react-router`. + +## Importing a Component + +This project setup supports ES6 modules thanks to Babel.<br> +While you can still use `require()` and `module.exports`, we encourage you to use [`import` and `export`](http://exploringjs.com/es6/ch_modules.html) instead. + +For example: + +### `Button.js` + +```js +import React, { Component } from 'react'; + +class Button extends Component { + render() { + // ... + } +} + +export default Button; // Don’t forget to use export default! +``` + +### `DangerButton.js` + + +```js +import React, { Component } from 'react'; +import Button from './Button'; // Import a component from another file + +class DangerButton extends Component { + render() { + return <Button color="red" />; + } +} + +export default DangerButton; +``` + +Be aware of the [difference between default and named exports](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281). It is a common source of mistakes. + +We suggest that you stick to using default imports and exports when a module only exports a single thing (for example, a component). That’s what you get when you use `export default Button` and `import Button from './Button'`. + +Named exports are useful for utility modules that export several functions. A module may have at most one default export and as many named exports as you like. + +Learn more about ES6 modules: + +* [When to use the curly braces?](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281) +* [Exploring ES6: Modules](http://exploringjs.com/es6/ch_modules.html) +* [Understanding ES6: Modules](https://leanpub.com/understandinges6/read#leanpub-auto-encapsulating-code-with-modules) + +## Code Splitting + +Instead of downloading the entire app before users can use it, code splitting allows you to split your code into small chunks which you can then load on demand. + +This project setup supports code splitting via [dynamic `import()`](http://2ality.com/2017/01/import-operator.html#loading-code-on-demand). Its [proposal](https://github.com/tc39/proposal-dynamic-import) is in stage 3. The `import()` function-like form takes the module name as an argument and returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which always resolves to the namespace object of the module. + +Here is an example: + +### `moduleA.js` + +```js +const moduleA = 'Hello'; + +export { moduleA }; +``` +### `App.js` + +```js +import React, { Component } from 'react'; + +class App extends Component { + handleClick = () => { + import('./moduleA') + .then(({ moduleA }) => { + // Use moduleA + }) + .catch(err => { + // Handle failure + }); + }; + + render() { + return ( + <div> + <button onClick={this.handleClick}>Load</button> + </div> + ); + } +} + +export default App; +``` + +This will make `moduleA.js` and all its unique dependencies as a separate chunk that only loads after the user clicks the 'Load' button. + +You can also use it with `async` / `await` syntax if you prefer it. + +### With React Router + +If you are using React Router check out [this tutorial](http://serverless-stack.com/chapters/code-splitting-in-create-react-app.html) on how to use code splitting with it. You can find the companion GitHub repository [here](https://github.com/AnomalyInnovations/serverless-stack-demo-client/tree/code-splitting-in-create-react-app). + +## Adding a Stylesheet + +This project setup uses [Webpack](https://webpack.js.org/) for handling all assets. Webpack offers a custom way of “extending” the concept of `import` beyond JavaScript. To express that a JavaScript file depends on a CSS file, you need to **import the CSS from the JavaScript file**: + +### `Button.css` + +```css +.Button { + padding: 20px; +} +``` + +### `Button.js` + +```js +import React, { Component } from 'react'; +import './Button.css'; // Tell Webpack that Button.js uses these styles + +class Button extends Component { + render() { + // You can use them as regular CSS styles + return <div className="Button" />; + } +} +``` + +**This is not required for React** but many people find this feature convenient. You can read about the benefits of this approach [here](https://medium.com/seek-ui-engineering/block-element-modifying-your-javascript-components-d7f99fcab52b). However you should be aware that this makes your code less portable to other build tools and environments than Webpack. + +In development, expressing dependencies this way allows your styles to be reloaded on the fly as you edit them. In production, all CSS files will be concatenated into a single minified `.css` file in the build output. + +If you are concerned about using Webpack-specific semantics, you can put all your CSS right into `src/index.css`. It would still be imported from `src/index.js`, but you could always remove that import if you later migrate to a different build tool. + +## Post-Processing CSS + +This project setup minifies your CSS and adds vendor prefixes to it automatically through [Autoprefixer](https://github.com/postcss/autoprefixer) so you don’t need to worry about it. + +For example, this: + +```css +.App { + display: flex; + flex-direction: row; + align-items: center; +} +``` + +becomes this: + +```css +.App { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} +``` + +If you need to disable autoprefixing for some reason, [follow this section](https://github.com/postcss/autoprefixer#disabling). + +## Adding a CSS Preprocessor (Sass, Less etc.) + +Generally, we recommend that you don’t reuse the same CSS classes across different components. For example, instead of using a `.Button` CSS class in `<AcceptButton>` and `<RejectButton>` components, we recommend creating a `<Button>` component with its own `.Button` styles, that both `<AcceptButton>` and `<RejectButton>` can render (but [not inherit](https://facebook.github.io/react/docs/composition-vs-inheritance.html)). + +Following this rule often makes CSS preprocessors less useful, as features like mixins and nesting are replaced by component composition. You can, however, integrate a CSS preprocessor if you find it valuable. In this walkthrough, we will be using Sass, but you can also use Less, or another alternative. + +First, let’s install the command-line interface for Sass: + +```sh +npm install --save node-sass-chokidar +``` + +Alternatively you may use `yarn`: + +```sh +yarn add node-sass-chokidar +``` + +Then in `package.json`, add the following lines to `scripts`: + +```diff + "scripts": { ++ "build-css": "node-sass-chokidar src/ -o src/", ++ "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive", + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", +``` + +>Note: To use a different preprocessor, replace `build-css` and `watch-css` commands according to your preprocessor’s documentation. + +Now you can rename `src/App.css` to `src/App.scss` and run `npm run watch-css`. The watcher will find every Sass file in `src` subdirectories, and create a corresponding CSS file next to it, in our case overwriting `src/App.css`. Since `src/App.js` still imports `src/App.css`, the styles become a part of your application. You can now edit `src/App.scss`, and `src/App.css` will be regenerated. + +To share variables between Sass files, you can use Sass imports. For example, `src/App.scss` and other component style files could include `@import "./shared.scss";` with variable definitions. + +To enable importing files without using relative paths, you can add the `--include-path` option to the command in `package.json`. + +``` +"build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/", +"watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive", +``` + +This will allow you to do imports like + +```scss +@import 'styles/_colors.scss'; // assuming a styles directory under src/ +@import 'nprogress/nprogress'; // importing a css file from the nprogress node module +``` + +At this point you might want to remove all CSS files from the source control, and add `src/**/*.css` to your `.gitignore` file. It is generally a good practice to keep the build products outside of the source control. + +As a final step, you may find it convenient to run `watch-css` automatically with `npm start`, and run `build-css` as a part of `npm run build`. You can use the `&&` operator to execute two scripts sequentially. However, there is no cross-platform way to run two scripts in parallel, so we will install a package for this: + +```sh +npm install --save npm-run-all +``` + +Alternatively you may use `yarn`: + +```sh +yarn add npm-run-all +``` + +Then we can change `start` and `build` scripts to include the CSS preprocessor commands: + +```diff + "scripts": { + "build-css": "node-sass-chokidar src/ -o src/", + "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive", +- "start": "react-scripts start", +- "build": "react-scripts build", ++ "start-js": "react-scripts start", ++ "start": "npm-run-all -p watch-css start-js", ++ "build-js": "react-scripts build", ++ "build": "npm-run-all build-css build-js", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +``` + +Now running `npm start` and `npm run build` also builds Sass files. + +**Why `node-sass-chokidar`?** + +`node-sass` has been reported as having the following issues: + +- `node-sass --watch` has been reported to have *performance issues* in certain conditions when used in a virtual machine or with docker. + +- Infinite styles compiling [#1939](https://github.com/facebookincubator/create-react-app/issues/1939) + +- `node-sass` has been reported as having issues with detecting new files in a directory [#1891](https://github.com/sass/node-sass/issues/1891) + + `node-sass-chokidar` is used here as it addresses these issues. + +## Adding Images, Fonts, and Files + +With Webpack, using static assets like images and fonts works similarly to CSS. + +You can **`import` a file right in a JavaScript module**. This tells Webpack to include that file in the bundle. Unlike CSS imports, importing a file gives you a string value. This value is the final path you can reference in your code, e.g. as the `src` attribute of an image or the `href` of a link to a PDF. + +To reduce the number of requests to the server, importing images that are less than 10,000 bytes returns a [data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) instead of a path. This applies to the following file extensions: bmp, gif, jpg, jpeg, and png. SVG files are excluded due to [#1153](https://github.com/facebookincubator/create-react-app/issues/1153). + +Here is an example: + +```js +import React from 'react'; +import logo from './logo.png'; // Tell Webpack this JS file uses this image + +console.log(logo); // /logo.84287d09.png + +function Header() { + // Import result is the URL of your image + return <img src={logo} alt="Logo" />; +} + +export default Header; +``` + +This ensures that when the project is built, Webpack will correctly move the images into the build folder, and provide us with correct paths. + +This works in CSS too: + +```css +.Logo { + background-image: url(./logo.png); +} +``` + +Webpack finds all relative module references in CSS (they start with `./`) and replaces them with the final paths from the compiled bundle. If you make a typo or accidentally delete an important file, you will see a compilation error, just like when you import a non-existent JavaScript module. The final filenames in the compiled bundle are generated by Webpack from content hashes. If the file content changes in the future, Webpack will give it a different name in production so you don’t need to worry about long-term caching of assets. + +Please be advised that this is also a custom feature of Webpack. + +**It is not required for React** but many people enjoy it (and React Native uses a similar mechanism for images).<br> +An alternative way of handling static assets is described in the next section. + +## Using the `public` Folder + +>Note: this feature is available with `react-scripts@0.5.0` and higher. + +### Changing the HTML + +The `public` folder contains the HTML file so you can tweak it, for example, to [set the page title](#changing-the-page-title). +The `<script>` tag with the compiled code will be added to it automatically during the build process. + +### Adding Assets Outside of the Module System + +You can also add other assets to the `public` folder. + +Note that we normally encourage you to `import` assets in JavaScript files instead. +For example, see the sections on [adding a stylesheet](#adding-a-stylesheet) and [adding images and fonts](#adding-images-fonts-and-files). +This mechanism provides a number of benefits: + +* Scripts and stylesheets get minified and bundled together to avoid extra network requests. +* Missing files cause compilation errors instead of 404 errors for your users. +* Result filenames include content hashes so you don’t need to worry about browsers caching their old versions. + +However there is an **escape hatch** that you can use to add an asset outside of the module system. + +If you put a file into the `public` folder, it will **not** be processed by Webpack. Instead it will be copied into the build folder untouched. To reference assets in the `public` folder, you need to use a special variable called `PUBLIC_URL`. + +Inside `index.html`, you can use it like this: + +```html +<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> +``` + +Only files inside the `public` folder will be accessible by `%PUBLIC_URL%` prefix. If you need to use a file from `src` or `node_modules`, you’ll have to copy it there to explicitly specify your intention to make this file a part of the build. + +When you run `npm run build`, Create React App will substitute `%PUBLIC_URL%` with a correct absolute path so your project works even if you use client-side routing or host it at a non-root URL. + +In JavaScript code, you can use `process.env.PUBLIC_URL` for similar purposes: + +```js +render() { + // Note: this is an escape hatch and should be used sparingly! + // Normally we recommend using `import` for getting asset URLs + // as described in “Adding Images and Fonts” above this section. + return <img src={process.env.PUBLIC_URL + '/img/logo.png'} />; +} +``` + +Keep in mind the downsides of this approach: + +* None of the files in `public` folder get post-processed or minified. +* Missing files will not be called at compilation time, and will cause 404 errors for your users. +* Result filenames won’t include content hashes so you’ll need to add query arguments or rename them every time they change. + +### When to Use the `public` Folder + +Normally we recommend importing [stylesheets](#adding-a-stylesheet), [images, and fonts](#adding-images-fonts-and-files) from JavaScript. +The `public` folder is useful as a workaround for a number of less common cases: + +* You need a file with a specific name in the build output, such as [`manifest.webmanifest`](https://developer.mozilla.org/en-US/docs/Web/Manifest). +* You have thousands of images and need to dynamically reference their paths. +* You want to include a small script like [`pace.js`](http://github.hubspot.com/pace/docs/welcome/) outside of the bundled code. +* Some library may be incompatible with Webpack and you have no other option but to include it as a `<script>` tag. + +Note that if you add a `<script>` that declares global variables, you also need to read the next section on using them. + +## Using Global Variables + +When you include a script in the HTML file that defines global variables and try to use one of these variables in the code, the linter will complain because it cannot see the definition of the variable. + +You can avoid this by reading the global variable explicitly from the `window` object, for example: + +```js +const $ = window.$; +``` + +This makes it obvious you are using a global variable intentionally rather than because of a typo. + +Alternatively, you can force the linter to ignore any line by adding `// eslint-disable-line` after it. + +## Adding Bootstrap + +You don’t have to use [Reactstrap](https://reactstrap.github.io/) together with React but it is a popular library for integrating Bootstrap with React apps. If you need it, you can integrate it with Create React App by following these steps: + +Install Reactstrap and Bootstrap from npm. React Bootstrap does not include Bootstrap CSS so this needs to be installed as well: + +```sh +npm install --save reactstrap bootstrap@4 +``` + +Alternatively you may use `yarn`: + +```sh +yarn add reactstrap bootstrap@4 +``` + +Import Bootstrap CSS and optionally Bootstrap theme CSS in the beginning of your ```src/index.js``` file: + +```js +import 'bootstrap/dist/css/bootstrap.css'; +// Put any other imports below so that CSS from your +// components takes precedence over default styles. +``` + +Import required React Bootstrap components within ```src/App.js``` file or your custom component files: + +```js +import { Navbar, Button } from 'reactstrap'; +``` + +Now you are ready to use the imported React Bootstrap components within your component hierarchy defined in the render method. Here is an example [`App.js`](https://gist.githubusercontent.com/gaearon/85d8c067f6af1e56277c82d19fd4da7b/raw/6158dd991b67284e9fc8d70b9d973efe87659d72/App.js) redone using React Bootstrap. + +### Using a Custom Theme + +Sometimes you might need to tweak the visual styles of Bootstrap (or equivalent package).<br> +We suggest the following approach: + +* Create a new package that depends on the package you wish to customize, e.g. Bootstrap. +* Add the necessary build steps to tweak the theme, and publish your package on npm. +* Install your own theme npm package as a dependency of your app. + +Here is an example of adding a [customized Bootstrap](https://medium.com/@tacomanator/customizing-create-react-app-aa9ffb88165) that follows these steps. + +## Adding Flow + +Flow is a static type checker that helps you write code with fewer bugs. Check out this [introduction to using static types in JavaScript](https://medium.com/@preethikasireddy/why-use-static-types-in-javascript-part-1-8382da1e0adb) if you are new to this concept. + +Recent versions of [Flow](http://flowtype.org/) work with Create React App projects out of the box. + +To add Flow to a Create React App project, follow these steps: + +1. Run `npm install --save flow-bin` (or `yarn add flow-bin`). +2. Add `"flow": "flow"` to the `scripts` section of your `package.json`. +3. Run `npm run flow init` (or `yarn flow init`) to create a [`.flowconfig` file](https://flowtype.org/docs/advanced-configuration.html) in the root directory. +4. Add `// @flow` to any files you want to type check (for example, to `src/App.js`). + +Now you can run `npm run flow` (or `yarn flow`) to check the files for type errors. +You can optionally use an IDE like [Nuclide](https://nuclide.io/docs/languages/flow/) for a better integrated experience. +In the future we plan to integrate it into Create React App even more closely. + +To learn more about Flow, check out [its documentation](https://flowtype.org/). + +## Adding Custom Environment Variables + +>Note: this feature is available with `react-scripts@0.2.3` and higher. + +Your project can consume variables declared in your environment as if they were declared locally in your JS files. By +default you will have `NODE_ENV` defined for you, and any other environment variables starting with +`REACT_APP_`. + +**The environment variables are embedded during the build time**. Since Create React App produces a static HTML/CSS/JS bundle, it can’t possibly read them at runtime. To read them at runtime, you would need to load HTML into memory on the server and replace placeholders in runtime, just like [described here](#injecting-data-from-the-server-into-the-page). Alternatively you can rebuild the app on the server anytime you change them. + +>Note: You must create custom environment variables beginning with `REACT_APP_`. Any other variables except `NODE_ENV` will be ignored to avoid accidentally [exposing a private key on the machine that could have the same name](https://github.com/facebookincubator/create-react-app/issues/865#issuecomment-252199527). Changing any environment variables will require you to restart the development server if it is running. + +These environment variables will be defined for you on `process.env`. For example, having an environment +variable named `REACT_APP_SECRET_CODE` will be exposed in your JS as `process.env.REACT_APP_SECRET_CODE`. + +There is also a special built-in environment variable called `NODE_ENV`. You can read it from `process.env.NODE_ENV`. When you run `npm start`, it is always equal to `'development'`, when you run `npm test` it is always equal to `'test'`, and when you run `npm run build` to make a production bundle, it is always equal to `'production'`. **You cannot override `NODE_ENV` manually.** This prevents developers from accidentally deploying a slow development build to production. + +These environment variables can be useful for displaying information conditionally based on where the project is +deployed or consuming sensitive data that lives outside of version control. + +First, you need to have environment variables defined. For example, let’s say you wanted to consume a secret defined +in the environment inside a `<form>`: + +```jsx +render() { + return ( + <div> + <small>You are running this application in <b>{process.env.NODE_ENV}</b> mode.</small> + <form> + <input type="hidden" defaultValue={process.env.REACT_APP_SECRET_CODE} /> + </form> + </div> + ); +} +``` + +During the build, `process.env.REACT_APP_SECRET_CODE` will be replaced with the current value of the `REACT_APP_SECRET_CODE` environment variable. Remember that the `NODE_ENV` variable will be set for you automatically. + +When you load the app in the browser and inspect the `<input>`, you will see its value set to `abcdef`, and the bold text will show the environment provided when using `npm start`: + +```html +<div> + <small>You are running this application in <b>development</b> mode.</small> + <form> + <input type="hidden" value="abcdef" /> + </form> +</div> +``` + +The above form is looking for a variable called `REACT_APP_SECRET_CODE` from the environment. In order to consume this +value, we need to have it defined in the environment. This can be done using two ways: either in your shell or in +a `.env` file. Both of these ways are described in the next few sections. + +Having access to the `NODE_ENV` is also useful for performing actions conditionally: + +```js +if (process.env.NODE_ENV !== 'production') { + analytics.disable(); +} +``` + +When you compile the app with `npm run build`, the minification step will strip out this condition, and the resulting bundle will be smaller. + +### Referencing Environment Variables in the HTML + +>Note: this feature is available with `react-scripts@0.9.0` and higher. + +You can also access the environment variables starting with `REACT_APP_` in the `public/index.html`. For example: + +```html +<title>%REACT_APP_WEBSITE_NAME% +``` + +Note that the caveats from the above section apply: + +* Apart from a few built-in variables (`NODE_ENV` and `PUBLIC_URL`), variable names must start with `REACT_APP_` to work. +* The environment variables are injected at build time. If you need to inject them at runtime, [follow this approach instead](#generating-dynamic-meta-tags-on-the-server). + +### Adding Temporary Environment Variables In Your Shell + +Defining environment variables can vary between OSes. It’s also important to know that this manner is temporary for the +life of the shell session. + +#### Windows (cmd.exe) + +```cmd +set REACT_APP_SECRET_CODE=abcdef&&npm start +``` + +(Note: the lack of whitespace is intentional.) + +#### Linux, macOS (Bash) + +```bash +REACT_APP_SECRET_CODE=abcdef npm start +``` + +### Adding Development Environment Variables In `.env` + +>Note: this feature is available with `react-scripts@0.5.0` and higher. + +To define permanent environment variables, create a file called `.env` in the root of your project: + +``` +REACT_APP_SECRET_CODE=abcdef +``` + +`.env` files **should be** checked into source control (with the exclusion of `.env*.local`). + +#### What other `.env` files can be used? + +>Note: this feature is **available with `react-scripts@1.0.0` and higher**. + +* `.env`: Default. +* `.env.local`: Local overrides. **This file is loaded for all environments except test.** +* `.env.development`, `.env.test`, `.env.production`: Environment-specific settings. +* `.env.development.local`, `.env.test.local`, `.env.production.local`: Local overrides of environment-specific settings. + +Files on the left have more priority than files on the right: + +* `npm start`: `.env.development.local`, `.env.development`, `.env.local`, `.env` +* `npm run build`: `.env.production.local`, `.env.production`, `.env.local`, `.env` +* `npm test`: `.env.test.local`, `.env.test`, `.env` (note `.env.local` is missing) + +These variables will act as the defaults if the machine does not explicitly set them.
+Please refer to the [dotenv documentation](https://github.com/motdotla/dotenv) for more details. + +>Note: If you are defining environment variables for development, your CI and/or hosting platform will most likely need +these defined as well. Consult their documentation how to do this. For example, see the documentation for [Travis CI](https://docs.travis-ci.com/user/environment-variables/) or [Heroku](https://devcenter.heroku.com/articles/config-vars). + +## Can I Use Decorators? + +Many popular libraries use [decorators](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841) in their documentation.
+Create React App doesn’t support decorator syntax at the moment because: + +* It is an experimental proposal and is subject to change. +* The current specification version is not officially supported by Babel. +* If the specification changes, we won’t be able to write a codemod because we don’t use them internally at Facebook. + +However in many cases you can rewrite decorator-based code without decorators just as fine.
+Please refer to these two threads for reference: + +* [#214](https://github.com/facebookincubator/create-react-app/issues/214) +* [#411](https://github.com/facebookincubator/create-react-app/issues/411) + +Create React App will add decorator support when the specification advances to a stable stage. + +## Integrating with an API Backend + +These tutorials will help you to integrate your app with an API backend running on another port, +using `fetch()` to access it. + +### Node +Check out [this tutorial](https://www.fullstackreact.com/articles/using-create-react-app-with-a-server/). +You can find the companion GitHub repository [here](https://github.com/fullstackreact/food-lookup-demo). + +### Ruby on Rails + +Check out [this tutorial](https://www.fullstackreact.com/articles/how-to-get-create-react-app-to-work-with-your-rails-api/). +You can find the companion GitHub repository [here](https://github.com/fullstackreact/food-lookup-demo-rails). + +## Proxying API Requests in Development + +>Note: this feature is available with `react-scripts@0.2.3` and higher. + +People often serve the front-end React app from the same host and port as their backend implementation.
+For example, a production setup might look like this after the app is deployed: + +``` +/ - static server returns index.html with React app +/todos - static server returns index.html with React app +/api/todos - server handles any /api/* requests using the backend implementation +``` + +Such setup is **not** required. However, if you **do** have a setup like this, it is convenient to write requests like `fetch('/api/todos')` without worrying about redirecting them to another host or port during development. + +To tell the development server to proxy any unknown requests to your API server in development, add a `proxy` field to your `package.json`, for example: + +```js + "proxy": "http://localhost:4000", +``` + +This way, when you `fetch('/api/todos')` in development, the development server will recognize that it’s not a static asset, and will proxy your request to `http://localhost:4000/api/todos` as a fallback. The development server will **only** attempt to send requests without `text/html` in its `Accept` header to the proxy. + +Conveniently, this avoids [CORS issues](http://stackoverflow.com/questions/21854516/understanding-ajax-cors-and-security-considerations) and error messages like this in development: + +``` +Fetch API cannot load http://localhost:4000/api/todos. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. +``` + +Keep in mind that `proxy` only has effect in development (with `npm start`), and it is up to you to ensure that URLs like `/api/todos` point to the right thing in production. You don’t have to use the `/api` prefix. Any unrecognized request without a `text/html` accept header will be redirected to the specified `proxy`. + +The `proxy` option supports HTTP, HTTPS and WebSocket connections.
+If the `proxy` option is **not** flexible enough for you, alternatively you can: + +* [Configure the proxy yourself](#configuring-the-proxy-manually) +* Enable CORS on your server ([here’s how to do it for Express](http://enable-cors.org/server_expressjs.html)). +* Use [environment variables](#adding-custom-environment-variables) to inject the right server host and port into your app. + +### "Invalid Host Header" Errors After Configuring Proxy + +When you enable the `proxy` option, you opt into a more strict set of host checks. This is necessary because leaving the backend open to remote hosts makes your computer vulnerable to DNS rebinding attacks. The issue is explained in [this article](https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a) and [this issue](https://github.com/webpack/webpack-dev-server/issues/887). + +This shouldn’t affect you when developing on `localhost`, but if you develop remotely like [described here](https://github.com/facebookincubator/create-react-app/issues/2271), you will see this error in the browser after enabling the `proxy` option: + +>Invalid Host header + +To work around it, you can specify your public development host in a file called `.env.development` in the root of your project: + +``` +HOST=mypublicdevhost.com +``` + +If you restart the development server now and load the app from the specified host, it should work. + +If you are still having issues or if you’re using a more exotic environment like a cloud editor, you can bypass the host check completely by adding a line to `.env.development.local`. **Note that this is dangerous and exposes your machine to remote code execution from malicious websites:** + +``` +# NOTE: THIS IS DANGEROUS! +# It exposes your machine to attacks from the websites you visit. +DANGEROUSLY_DISABLE_HOST_CHECK=true +``` + +We don’t recommend this approach. + +### Configuring the Proxy Manually + +>Note: this feature is available with `react-scripts@1.0.0` and higher. + +If the `proxy` option is **not** flexible enough for you, you can specify an object in the following form (in `package.json`).
+You may also specify any configuration value [`http-proxy-middleware`](https://github.com/chimurai/http-proxy-middleware#options) or [`http-proxy`](https://github.com/nodejitsu/node-http-proxy#options) supports. +```js +{ + // ... + "proxy": { + "/api": { + "target": "", + "ws": true + // ... + } + } + // ... +} +``` + +All requests matching this path will be proxies, no exceptions. This includes requests for `text/html`, which the standard `proxy` option does not proxy. + +If you need to specify multiple proxies, you may do so by specifying additional entries. +Matches are regular expressions, so that you can use a regexp to match multiple paths. +```js +{ + // ... + "proxy": { + // Matches any request starting with /api + "/api": { + "target": "", + "ws": true + // ... + }, + // Matches any request starting with /foo + "/foo": { + "target": "", + "ssl": true, + "pathRewrite": { + "^/foo": "/foo/beta" + } + // ... + }, + // Matches /bar/abc.html but not /bar/sub/def.html + "/bar/[^/]*[.]html": { + "target": "", + // ... + }, + // Matches /baz/abc.html and /baz/sub/def.html + "/baz/.*/.*[.]html": { + "target": "" + // ... + } + } + // ... +} +``` + +### Configuring a WebSocket Proxy + +When setting up a WebSocket proxy, there are a some extra considerations to be aware of. + +If you’re using a WebSocket engine like [Socket.io](https://socket.io/), you must have a Socket.io server running that you can use as the proxy target. Socket.io will not work with a standard WebSocket server. Specifically, don't expect Socket.io to work with [the websocket.org echo test](http://websocket.org/echo.html). + +There’s some good documentation available for [setting up a Socket.io server](https://socket.io/docs/). + +Standard WebSockets **will** work with a standard WebSocket server as well as the websocket.org echo test. You can use libraries like [ws](https://github.com/websockets/ws) for the server, with [native WebSockets in the browser](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). + +Either way, you can proxy WebSocket requests manually in `package.json`: + +```js +{ + // ... + "proxy": { + "/socket": { + // Your compatible WebSocket server + "target": "ws://", + // Tell http-proxy-middleware that this is a WebSocket proxy. + // Also allows you to proxy WebSocket requests without an additional HTTP request + // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade + "ws": true + // ... + } + } + // ... +} +``` + +## Using HTTPS in Development + +>Note: this feature is available with `react-scripts@0.4.0` and higher. + +You may require the dev server to serve pages over HTTPS. One particular case where this could be useful is when using [the "proxy" feature](#proxying-api-requests-in-development) to proxy requests to an API server when that API server is itself serving HTTPS. + +To do this, set the `HTTPS` environment variable to `true`, then start the dev server as usual with `npm start`: + +#### Windows (cmd.exe) + +```cmd +set HTTPS=true&&npm start +``` + +(Note: the lack of whitespace is intentional.) + +#### Linux, macOS (Bash) + +```bash +HTTPS=true npm start +``` + +Note that the server will use a self-signed certificate, so your web browser will almost definitely display a warning upon accessing the page. + +## Generating Dynamic `` Tags on the Server + +Since Create React App doesn’t support server rendering, you might be wondering how to make `` tags dynamic and reflect the current URL. To solve this, we recommend to add placeholders into the HTML, like this: + +```html + + + + + +``` + +Then, on the server, regardless of the backend you use, you can read `index.html` into memory and replace `__OG_TITLE__`, `__OG_DESCRIPTION__`, and any other placeholders with values depending on the current URL. Just make sure to sanitize and escape the interpolated values so that they are safe to embed into HTML! + +If you use a Node server, you can even share the route matching logic between the client and the server. However duplicating it also works fine in simple cases. + +## Pre-Rendering into Static HTML Files + +If you’re hosting your `build` with a static hosting provider you can use [react-snapshot](https://www.npmjs.com/package/react-snapshot) or [react-snap](https://github.com/stereobooster/react-snap) to generate HTML pages for each route, or relative link, in your application. These pages will then seamlessly become active, or “hydrated”, when the JavaScript bundle has loaded. + +There are also opportunities to use this outside of static hosting, to take the pressure off the server when generating and caching routes. + +The primary benefit of pre-rendering is that you get the core content of each page _with_ the HTML payload—regardless of whether or not your JavaScript bundle successfully downloads. It also increases the likelihood that each route of your application will be picked up by search engines. + +You can read more about [zero-configuration pre-rendering (also called snapshotting) here](https://medium.com/superhighfives/an-almost-static-stack-6df0a2791319). + +## Injecting Data from the Server into the Page + +Similarly to the previous section, you can leave some placeholders in the HTML that inject global variables, for example: + +```js + + + + +``` + +Then, on the server, you can replace `__SERVER_DATA__` with a JSON of real data right before sending the response. The client code can then read `window.SERVER_DATA` to use it. **Make sure to [sanitize the JSON before sending it to the client](https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0) as it makes your app vulnerable to XSS attacks.** + +## Running Tests + +>Note: this feature is available with `react-scripts@0.3.0` and higher.
+>[Read the migration guide to learn how to enable it in older projects!](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md#migrating-from-023-to-030) + +Create React App uses [Jest](https://facebook.github.io/jest/) as its test runner. To prepare for this integration, we did a [major revamp](https://facebook.github.io/jest/blog/2016/09/01/jest-15.html) of Jest so if you heard bad things about it years ago, give it another try. + +Jest is a Node-based runner. This means that the tests always run in a Node environment and not in a real browser. This lets us enable fast iteration speed and prevent flakiness. + +While Jest provides browser globals such as `window` thanks to [jsdom](https://github.com/tmpvar/jsdom), they are only approximations of the real browser behavior. Jest is intended to be used for unit tests of your logic and your components rather than the DOM quirks. + +We recommend that you use a separate tool for browser end-to-end tests if you need them. They are beyond the scope of Create React App. + +### Filename Conventions + +Jest will look for test files with any of the following popular naming conventions: + +* Files with `.js` suffix in `__tests__` folders. +* Files with `.test.js` suffix. +* Files with `.spec.js` suffix. + +The `.test.js` / `.spec.js` files (or the `__tests__` folders) can be located at any depth under the `src` top level folder. + +We recommend to put the test files (or `__tests__` folders) next to the code they are testing so that relative imports appear shorter. For example, if `App.test.js` and `App.js` are in the same folder, the test just needs to `import App from './App'` instead of a long relative path. Colocation also helps find tests more quickly in larger projects. + +### Command Line Interface + +When you run `npm test`, Jest will launch in the watch mode. Every time you save a file, it will re-run the tests, just like `npm start` recompiles the code. + +The watcher includes an interactive command-line interface with the ability to run all tests, or focus on a search pattern. It is designed this way so that you can keep it open and enjoy fast re-runs. You can learn the commands from the “Watch Usage” note that the watcher prints after every run: + +![Jest watch mode](http://facebook.github.io/jest/img/blog/15-watch.gif) + +### Version Control Integration + +By default, when you run `npm test`, Jest will only run the tests related to files changed since the last commit. This is an optimization designed to make your tests run fast regardless of how many tests you have. However it assumes that you don’t often commit the code that doesn’t pass the tests. + +Jest will always explicitly mention that it only ran tests related to the files changed since the last commit. You can also press `a` in the watch mode to force Jest to run all tests. + +Jest will always run all tests on a [continuous integration](#continuous-integration) server or if the project is not inside a Git or Mercurial repository. + +### Writing Tests + +To create tests, add `it()` (or `test()`) blocks with the name of the test and its code. You may optionally wrap them in `describe()` blocks for logical grouping but this is neither required nor recommended. + +Jest provides a built-in `expect()` global function for making assertions. A basic test could look like this: + +```js +import sum from './sum'; + +it('sums numbers', () => { + expect(sum(1, 2)).toEqual(3); + expect(sum(2, 2)).toEqual(4); +}); +``` + +All `expect()` matchers supported by Jest are [extensively documented here](https://facebook.github.io/jest/docs/en/expect.html#content).
+You can also use [`jest.fn()` and `expect(fn).toBeCalled()`](https://facebook.github.io/jest/docs/en/expect.html#tohavebeencalled) to create “spies” or mock functions. + +### Testing Components + +There is a broad spectrum of component testing techniques. They range from a “smoke test” verifying that a component renders without throwing, to shallow rendering and testing some of the output, to full rendering and testing component lifecycle and state changes. + +Different projects choose different testing tradeoffs based on how often components change, and how much logic they contain. If you haven’t decided on a testing strategy yet, we recommend that you start with creating simple smoke tests for your components: + +```js +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); +}); +``` + +This test mounts a component and makes sure that it didn’t throw during rendering. Tests like this provide a lot value with very little effort so they are great as a starting point, and this is the test you will find in `src/App.test.js`. + +When you encounter bugs caused by changing components, you will gain a deeper insight into which parts of them are worth testing in your application. This might be a good time to introduce more specific tests asserting specific expected output or behavior. + +If you’d like to test components in isolation from the child components they render, we recommend using [`shallow()` rendering API](http://airbnb.io/enzyme/docs/api/shallow.html) from [Enzyme](http://airbnb.io/enzyme/). To install it, run: + +```sh +npm install --save enzyme enzyme-adapter-react-16 react-test-renderer +``` + +Alternatively you may use `yarn`: + +```sh +yarn add enzyme enzyme-adapter-react-16 react-test-renderer +``` + +As of Enzyme 3, you will need to install Enzyme along with an Adapter corresponding to the version of React you are using. (The examples above use the adapter for React 16.) + +The adapter will also need to be configured in your [global setup file](#initializing-test-environment): + +#### `src/setupTests.js` +```js +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +configure({ adapter: new Adapter() }); +``` + +Now you can write a smoke test with it: + +```js +import React from 'react'; +import { shallow } from 'enzyme'; +import App from './App'; + +it('renders without crashing', () => { + shallow(); +}); +``` + +Unlike the previous smoke test using `ReactDOM.render()`, this test only renders `` and doesn’t go deeper. For example, even if `` itself renders a ` + + + ); +} + +export default CmdLogDisplayArea; diff --git a/aspnetapp/WINGS/ClientApp/src/components/command/log_display/CmdLogTabPanel.tsx b/aspnetapp/WINGS/ClientApp/src/components/command/log_display/CmdLogTabPanel.tsx new file mode 100644 index 0000000..dd65dba --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/command/log_display/CmdLogTabPanel.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { createStyles, makeStyles, Theme } from '@material-ui/core'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import { CommandFileLineLogs } from '../../../models'; +import CmdLogTableRow from './CmdLogTableRow'; + +const useStyles = makeStyles( + createStyles({ + container: { + width: 850, + maxHeight: 700, + }, + tableEventShifter: { + position: "absolute", + zIndex: -10, + outline: 0 + } +})); + +export interface CmdLogTabPanelProps { + content: CommandFileLineLogs[] +} + +const CmdLogTabPanel = (props: CmdLogTabPanelProps) => { + const { content } = props; + const classes = useStyles(); + + return ( +
+ + + + + + Time + Command Log + + + + {content.length >= 1 && ( + content.map((line, i) => ( + + )) + )} + +
+
+
+ ); +} + +export default CmdLogTabPanel; diff --git a/aspnetapp/WINGS/ClientApp/src/components/command/log_display/CmdLogTableRow.tsx b/aspnetapp/WINGS/ClientApp/src/components/command/log_display/CmdLogTableRow.tsx new file mode 100644 index 0000000..8fa727f --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/command/log_display/CmdLogTableRow.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import TableCell from '@material-ui/core/TableCell'; +import TableRow from '@material-ui/core/TableRow'; +import { createStyles, makeStyles, Theme } from '@material-ui/core'; +import { CommandFileLineLogs } from '../../../models'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + row: { + height: 20 + }, + lineNumCell: { + width: 24, + padding: "6px 6px 6px 10px" + }, + stopCell: { + width: 10, + padding: 6 + }, + stopIcon: { + fill: theme.palette.primary.light, + width: "15px", + height: "15px", + verticalAlign: "middle" + }, + commanderCell: { + width: 20, + padding: 6 + }, + requestCell: { + padding: 6 + }, + isSelected: { + backgroundColor: theme.palette.grey[800] + }, + execSuccess: { + color: theme.palette.success.main + }, + comment: { + color: theme.palette.info.main + }, + error: { + color: theme.palette.error.main + } +})); + +export interface CmdLogTableRowProps { + line: CommandFileLineLogs, + index: number +} + +const CmdLogTableRow = (props: CmdLogTableRowProps) => { + const classes = useStyles(); + const spacer = ; + const errorMessage = "\"CommandExe\" : Command not found. Check the first word in this line."; + + const statusColor = (status: string) => { + if(status == "Success"){ + return classes.execSuccess; + } + else if(status == "Error"){ + return classes.error; + } + else{ + return ""; + } + } + + const showCmdLogContent = () => { + const content = props.line.content.replace(/"/g,""); + const status = props.line.status; + const commentIndex = content.indexOf("#"); + + if (commentIndex == 0){ + return

{content}

; + } + else if (commentIndex == -1) { + return

{(content!="")?content:errorMessage}

; + } + else { + let command = content.slice(0,commentIndex-1); + let message = content.slice(commentIndex); + return ( +

+ {command} + {spacer} + {message} +

+ ); + } + } + + return ( + + + {props.index+1} + + + {props.line.time} + + + {showCmdLogContent()} + + + ); +}; + +export default CmdLogTableRow; diff --git a/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/OpenPlanDialog.tsx b/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/OpenPlanDialog.tsx new file mode 100644 index 0000000..6d97ab5 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/OpenPlanDialog.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import Dialog from '@material-ui/core/Dialog'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../../redux/store/RootState'; +import { getCurrentOperation } from '../../../redux/operations/selectors'; +import { getAllIndexes } from '../../../redux/plans/selectors'; +import { openPlan } from '../../../redux/plans/operations'; +import FileTreeMultiView from '../../common/FileTreeMultiView'; + +const useStyles = makeStyles( + createStyles({ + paper: { + height: '60vh', + width: 500 + } +})); + +export interface OpenPlanDialogProps { + classes: Record<'paper', string>; + keepMounted: boolean; + open: boolean; + onClose: () => void; +} + +const OpenPlanDialog = (props: OpenPlanDialogProps) => { + const classes = useStyles(); + const selector = useSelector((state: RootState) => state); + const dispatch = useDispatch(); + const { onClose, open, ...other } = props; + + const indexes = getAllIndexes(selector); + const operation = getCurrentOperation(selector); + + + const [values, setValues] = React.useState([]); + + const handleCancel = () => { + onClose(); + setValues([]); + }; + + const handleOk = () => { + if (values != []) { + indexes.forEach(index => { + if (values.indexOf(index.id) >= 0) { + dispatch(openPlan(index.id)); + } + }) + } + onClose(); + setValues([]); + }; + + const rootPath = () => { + switch (operation.fileLocation) { + case 'Local': + if (indexes.length < 2) { + return ""; + } else { + return indexes[1].filePath.split('cmdplan')[0]+"cmdplan/"; + } + + default: + return ""; + } + } + + return ( + + Select Command File + + + + + + + + + ); +} + +export default OpenPlanDialog; diff --git a/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/PlanDisplayArea.tsx b/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/PlanDisplayArea.tsx new file mode 100644 index 0000000..a639d1c --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/PlanDisplayArea.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import Tabs from '@material-ui/core/Tabs'; +import Tab from '@material-ui/core/Tab'; +import Tooltip from '@material-ui/core/Tooltip'; +import AddIcon from '@material-ui/icons/Add'; +import CloseIcon from '@material-ui/icons/Close'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../../redux/store/RootState'; +import { getOpenedPlanIndexes, getOpenedPlanIds, getActivePlanId, getPlanContents, getInExecution } from '../../../redux/plans/selectors'; +import { activatePlanAction, closePlanAction } from '../../../redux/plans/actions'; +import OpenPlanDialog from './OpenPlanDialog'; +import PlanTabPanel from './PlanTabPanel'; +import IconButtonInTabs from '../../common/IconButtonInTabs'; +import { UNPLANNED_ID } from '../../../constants'; + +const useStyles = makeStyles( + createStyles({ + root: { + display: 'flex', + height: 700, + }, + tabs: { + borderRight: "1px solid", + width: 230 + }, + tab: { + width: 220, + minHeight: "auto", + textAlign: "left", + padding: "6px 0 6px 0", + "& span": { + whiteSpace: "nowrap", + textOverflow: "ellipsis", + overflow: "hidden", + display: "inline-block", + flexGrow: 0 + }, + "& .MuiTab-wrapper > *:first-child": { + marginBottom: 0, + padding: "0 5px 0 5px" + } + }, + dialogPaper: { + width: '80%', + maxHeight: 435, + }, +})); + +const a11yProps = (index: any) => { + return { + id: `vertical-tab-${index}`, + 'aria-controls': `vertical-tabpanel-${index}`, + }; +} + +const PlanDisplayArea = () => { + const classes = useStyles(); + const dispatch = useDispatch(); + const selector = useSelector((state: RootState) => state); + + const [dialogOpen, setDialogOpen] = React.useState(false); + + const openedIds = getOpenedPlanIds(selector); + const planIndexes = getOpenedPlanIndexes(selector); + const activePlanId = getActivePlanId(selector); + const planContents = getPlanContents(selector); + const inExecution = getInExecution(selector); + const value = openedIds.indexOf(activePlanId); + + const handleDialogOpen = () => { + if (!inExecution) { + setDialogOpen(true); + } + }; + + const handleDialogClose = () => { + setDialogOpen(false); + }; + + const handleValueChange = (event: React.ChangeEvent<{}>, value: number) => { + if (!inExecution) { + dispatch(activatePlanAction(openedIds[value])); + } + }; + + const closePlan = (id: string) => { + dispatch(closePlanAction(id)); + } + + interface CloseIconInTabProps { + onClick: (event: React.MouseEvent) => void + } + + const CloseIconInTab = (props: CloseIconInTabProps) => { + return ( + + + + ); + }; + + return ( +
+ + {planIndexes.length > 0 && ( + planIndexes.map((index,i) => ( + index.id === UNPLANNED_ID ? + + : ( + closePlan(index.id)}/>} + /> + ) + )) + )} + + + + + {planIndexes.length > 0 && ( + planIndexes.map((index,i) => ( + + )) + )} + +
+ ); +} + +export default PlanDisplayArea; diff --git a/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/PlanTabPanel.tsx b/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/PlanTabPanel.tsx new file mode 100644 index 0000000..961e316 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/PlanTabPanel.tsx @@ -0,0 +1,328 @@ +import React, { useRef } from 'react'; +import { makeStyles, createStyles } from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import { CommandPlanLine, RequestStatus } from '../../../models'; +import RequestTableRow from './RequestTableRow'; +import { selectedPlanRowAction, execRequestSuccessAction, execRequestErrorAction, execRequestsStartAction, execRequestsEndAction } from '../../../redux/plans/actions'; +import { getAllIndexes, getInExecution, getSelectedRow } from '../../../redux/plans/selectors'; +import { useDispatch, useSelector } from 'react-redux'; +import { openPlan, postCommand, postCommandFileLineLog } from '../../../redux/plans/operations'; +import { RootState } from '../../../redux/store/RootState'; +import { getLatestTelemetries } from '../../../redux/telemetries/selectors'; + +const useStyles = makeStyles( + createStyles({ + container: { + width: 700, + maxHeight: 700, + }, + tableEventShifter: { + position: "absolute", + zIndex: -10, + outline: 0 + } +})); + +const _sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export interface PlanTabPanelProps { + value: number, + index: number, + name: string, + content: CommandPlanLine[] +} + +const PlanTabPanel = (props: PlanTabPanelProps) => { + const {value, index, name, content } = props; + const classes = useStyles(); + const dispatch = useDispatch(); + const selector = useSelector((state: RootState) => state); + + const [lastSelectedRow, setLastSelectedRow] = React.useState(-1); + + let selectedRow = getSelectedRow(selector); + + if (index == value && selectedRow != lastSelectedRow) { + if (selectedRow == -1) { + if (lastSelectedRow >= 0) { + selectedRow = lastSelectedRow; + dispatch(selectedPlanRowAction(selectedRow)); + } else { + setLastSelectedRow(selectedRow); + } + } + else { + setLastSelectedRow(selectedRow); + } + } + const allIndexes = getAllIndexes(selector); + const inExecution = getInExecution(selector); + + const textInput = useRef() as React.MutableRefObject; + + const container = document.getElementById('plan-table-container'); + const tbody = document.getElementById('plan-table-body'); + + const advanceSelectedRow = (nextRow: number) => { + selectedRow < content.length-1 && dispatch(selectedPlanRowAction(nextRow)); + if (container && tbody) { + const trHeight = tbody.getElementsByTagName('tr')[0].clientHeight; + const top = Math.ceil(container.scrollTop/trHeight); + if ((nextRow - top) > 15) { + container.scrollTop += trHeight; + } + } + }; + + const backSelectedRow = (nextRow: number) => { + selectedRow > 0 && dispatch(selectedPlanRowAction(nextRow)); + if (container && tbody) { + const trHeight = tbody.getElementsByTagName('tr')[0].clientHeight; + const top = Math.ceil(container.scrollTop/trHeight); + if ((nextRow - top) < 4) { + container.scrollTop -= trHeight; + } + } + }; + + const handleTableClick = () => { + textInput.current.focus(); + } + + const handleRowClick = (i: number) => { + if (inExecution) return; + dispatch(selectedPlanRowAction(i)); + }; + + const handleKeyDown = async (event: React.KeyboardEvent) => { + if (inExecution) return; + if (event.key === 'ArrowDown') { + advanceSelectedRow(selectedRow+1); + } else if (event.key === 'ArrowUp') { + backSelectedRow(selectedRow-1); + } + if (event.key === 'Enter' && event.shiftKey) { + dispatch(execRequestsStartAction()); + await executeMultipleRequests() + dispatch(execRequestsEndAction()); + } + } + + const executeMultipleRequests = async () => { + let row = selectedRow; + do { + const exeret = await executeRequest(row); + await sendCmdFileLine(row, exeret); + if (content[row].request.method == "call") { + break; + } + row += 1; + advanceSelectedRow(row); + } while (row < content.length && !content[row].request.stopFlag) + if (row === content.length) { + dispatch(selectedPlanRowAction(-1)); + } + } + + const executeRequest = async (row: number): Promise => { + const req = content[row].request; + let exeret = false; + switch (req.type) { + case "comment": + await dispatch(execRequestSuccessAction(row)); + exeret = true; + break; + + case "command": + let commandret = [false]; + await dispatch(postCommand(row, req, commandret)); + exeret = commandret[0]; + break; + + case "control": + let controlret = await executeControlRequest(row); + exeret = controlret; + break; + + default: + break; + } + return exeret; + } + + const executeControlRequest = async (row: number): Promise => { + const req = content[row].request; + let reqret = false; + switch (req.method) { + case "wait_sec": + await _sleep(req.body.time * 1000); + dispatch(execRequestSuccessAction(row)); + reqret = true; + break; + + case "call": + const fileName = req.body.fileName; + const ret = allIndexes.findIndex(index => index.name === fileName); + if (ret !== -1) { + dispatch(execRequestSuccessAction(row)); + dispatch(openPlan(allIndexes[ret].id)); + reqret = true; + } else { + dispatch(execRequestErrorAction(row)); + reqret = false; + } + break; + + case "check_value": + const tlms = getLatestTelemetries(selector)[req.body.variable.split('.')[0]]; + var tlmidx = -1; + if (tlms.findIndex(index => index.telemetryInfo.name === req.body.variable) >= 0) { + tlmidx = tlms.findIndex(index => index.telemetryInfo.name === req.body.variable); + } else if (tlms.findIndex(index => index.telemetryInfo.name === req.body.variable.split('.').slice(1).join('.')) >= 0) { + tlmidx = tlms.findIndex(index => index.telemetryInfo.name === req.body.variable.split('.').slice(1).join('.')); + } else { + break; + } + const tlm = tlms[tlmidx]; + const tlmValue = parseTlmValue(tlm.telemetryValue.value, tlm.telemetryInfo.convType); + const comparedValue = parseTlmValue(req.body.value, tlm.telemetryInfo.convType); + switch (req.body.compare) { + case "==": + if (tlmValue === comparedValue) { + dispatch(execRequestSuccessAction(row)); + reqret = true; + } else { + dispatch(execRequestErrorAction(row)); + reqret = false; + } + break; + case ">=": + if (tlmValue >= comparedValue) { + dispatch(execRequestSuccessAction(row)); + reqret = true; + } else { + dispatch(execRequestErrorAction(row)); + reqret = false; + } + break; + case "<=": + if (tlmValue <= comparedValue) { + dispatch(execRequestSuccessAction(row)); + reqret = true; + } else { + dispatch(execRequestErrorAction(row)); + reqret = false; + } + break; + case ">": + if (tlmValue > comparedValue) { + dispatch(execRequestSuccessAction(row)); + reqret = true; + } else { + dispatch(execRequestErrorAction(row)); + reqret = false; + } + break; + case "<": + if (tlmValue < comparedValue) { + dispatch(execRequestSuccessAction(row)); + reqret = true; + } else { + dispatch(execRequestErrorAction(row)); + reqret = false; + } + break; + case "!=": + if (tlmValue !== comparedValue) { + dispatch(execRequestSuccessAction(row)); + reqret = true; + } else { + dispatch(execRequestErrorAction(row)); + reqret = false; + } + break; + default: + dispatch(execRequestErrorAction(row)); + reqret = false; + break; + } + break; + + default: + break; + } + return reqret; + }; + + const parseTlmValue = (value: string, convType: string) => { + switch (convType) { + case "NONE": + return Number(value); + case "POLY": + return Number(value); + case "STATUS": + return value; + case "HEX": + return value; + default: + return value; + } + } + + const sendCmdFileLine = async (row: number, ret: boolean) => { + const status_tmp: RequestStatus = (ret) ? { success: true, error: false } : { success: false, error: true }; + const content_tmp: CommandPlanLine = { request: content[row].request, status: status_tmp }; + await dispatch(postCommandFileLineLog(content_tmp)); + } + + if (value !== index) { + return <> + }; + + return ( +
+ handleKeyDown(event)} + readOnly + > + + + + + + + + {name} + + + + + {content.length > 0 && ( + content.map((line, i) => ( + handleRowClick(i)} + /> + )) + )} + +
+
+
+ ); +} + +export default PlanTabPanel; diff --git a/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/RequestTableRow.tsx b/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/RequestTableRow.tsx new file mode 100644 index 0000000..61f4ddb --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/command/plan_display/RequestTableRow.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { Command, CommandPlanLine, Request, RequestStatus } from '../../../models'; +import TableCell from '@material-ui/core/TableCell'; +import TableRow from '@material-ui/core/TableRow'; +import StopIcon from '@material-ui/icons/Stop'; +import { createStyles, makeStyles, Theme } from '@material-ui/core'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + row: { + height: 20 + }, + lineNumCell: { + width: 24, + padding: "6px 6px 6px 10px" + }, + stopCell: { + width: 10, + padding: 6 + }, + stopIcon: { + fill: theme.palette.primary.light, + width: "15px", + height: "15px", + verticalAlign: "middle" + }, + requestCell: { + padding: 6 + }, + isSelected: { + backgroundColor: theme.palette.grey[800] + }, + execSuccess: { + color: theme.palette.success.main + }, + comment: { + color: theme.palette.info.main + }, + error: { + color: theme.palette.error.main + } +})); + +export interface RequestTableRowProps { + line: CommandPlanLine, + index: number, + isSelected: boolean, + onClick: ((event: React.MouseEvent) => void) | undefined +} + +const RequestTableRow = (props: RequestTableRowProps) => { + const classes = useStyles(); + const spacer = ; + + const showCommandParam = (command: Command) => { + return ( + <> + {command.execType !== "RT" && (<>{spacer}{command.execTime})} + {command.params.length > 0 && ( + command.params.map((param,i) => ( + + {spacer}{param.value} + + )) + )} + + ) + }; + + const showControlBody = (request: Request) => { + const { method, body } = request; + + switch (method) { + case "wait_sec": + return <>{body.time} + + case "call": + return <>{body.fileName} + + case "check_value": + return <>{body.variable}{spacer}{body.compare}{spacer}{body.value} + + default: + return; + } + } + + const statusColor = (status: RequestStatus) => { + if (status.error) return classes.error; + if (status.success) return classes.execSuccess; + return ""; + } + + const showRequestContent = () => { + const req = props.line.request; + const status = props.line.status; + + if (req.syntaxError) { + return

{req.errorMessage}

; + } + + switch (req.type) { + case "comment": + return

{req.body}

; + + case "command": + const command = req.body; + return ( +

+ {(command.component) ? ((command.isViaMobc) ? "MOBC_" + command.execType + "_" + command.component + "_RT." : command.component + "_" + command.execType + "." ) : command.execType + "." } + {command.name} + {showCommandParam(command)} + {spacer} + {req.inlineComment} +

+ ); + + case "control": + return ( +

+ {req.method} + {spacer} + {showControlBody(req)} + {spacer} + {req.inlineComment} +

+ ) + + default: + return; + }; + } + + return ( + + + {props.index+1} + + + {props.line.request.stopFlag && } + + + {showRequestContent()} + + + + ); +}; + +export default RequestTableRow; diff --git a/aspnetapp/WINGS/ClientApp/src/components/command/plan_edit/CommandSelectArea.tsx b/aspnetapp/WINGS/ClientApp/src/components/command/plan_edit/CommandSelectArea.tsx new file mode 100644 index 0000000..a163776 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/command/plan_edit/CommandSelectArea.tsx @@ -0,0 +1,149 @@ +import React, { useEffect, useState } from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../../redux/store/RootState'; +import { getCommands, getTargets, getComponents } from '../../../redux/commands/selectors'; +import SelectBox, { SelectOption } from '../../common/SelectBox'; +import CheckBox from '../../common/CheckBox'; +import { selectedCommandEditAction, selectedCommandCommitAction, selectedTargetEditAction, selectedComponentEditAction } from '../../../redux/plans/actions'; +import { getSelectedCommand } from '../../../redux/plans/selectors'; +import { TARGET_ALL, COMPONENT_ALL } from '../../../constants'; +import SetParamTable from './SetParamTable'; + +const useStyles = makeStyles( + createStyles({ + root: { + marginLeft: 20, + width: 500 + }, + button: { + width: 120 + } +})); + +const execTypeOptions: SelectOption[] = ["RT","TL","BL","UTL"].map(type => ({id: type, name: type})); + +const CommandSelectionArea = () => { + const classes = useStyles(); + const selector = useSelector((state: RootState) => state); + const dispatch = useDispatch(); + + const commands = getCommands(selector); + const targets = getTargets(selector); + const components = getComponents(selector); + const { component, target, command } = getSelectedCommand(selector); + + const initIndexNum = commands.map(cmd => cmd.code).indexOf(command.code); + const initIndex = initIndexNum < 0 ? "" : String(initIndexNum); + const [commandIndex, setCommandIndex] = useState(initIndex); + + const [commandOptions, setCommandOptions] = useState([]); + + const targetOptions: SelectOption[] = targets.map(target => ({ id: target, name: target })); + + const componentOptions: SelectOption[] = components.map(component => ({ id: component, name: component })); + + useEffect(() => { + let options: SelectOption[] = []; + commands.map((command, i) => { + (target === TARGET_ALL || command.target === target) && (component === COMPONENT_ALL || command.component === component) && + options.push({id: String(i), name: command.name}) + }); + setCommandOptions(options); + }, [component, target]); + + const handleComponentChange = (component: string) => { + setCommandIndex(""); + dispatch(selectedComponentEditAction(component)); + } + + const handleIsViaMobcChange = (isViaMobc: boolean) => { + const newSelectedCommand = { + ...command, + isViaMobc: isViaMobc + }; + dispatch(selectedCommandEditAction(newSelectedCommand)); + } + + const handleExecTypeChange = (execType: string) => { + const newSelectedCommand = { + ...command, + execType: execType, + execTime: NaN + }; + dispatch(selectedCommandEditAction(newSelectedCommand)); + }; + + const handleTargetChange = (target: string) => { + setCommandIndex(""); + dispatch(selectedTargetEditAction(target)); + } + + const handleCommandIndexChange = (commandIndex: string) => { + setCommandIndex(commandIndex); + const index: number = +commandIndex; + const newSelectedCommand = { + ...commands[index], + execType: command.execType, + execTime: command.execTime, + isViaMobc: command.isViaMobc + } + dispatch(selectedCommandEditAction(newSelectedCommand)); + } + + const addUnplannedCommand = () => { + if (command.name === "") return; + if (command.params.map(param => param.value).every(value => value)) { + dispatch(selectedCommandCommitAction()); + } + } + + return ( +
+

Command Selection

+
+ + +
+ +
+ +
+ +
+ + {command.description} + +
+ +
+ + +
+ ) +} + +export default CommandSelectionArea; diff --git a/aspnetapp/WINGS/ClientApp/src/components/command/plan_edit/SetParamTable.tsx b/aspnetapp/WINGS/ClientApp/src/components/command/plan_edit/SetParamTable.tsx new file mode 100644 index 0000000..1a98ad3 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/command/plan_edit/SetParamTable.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import TextField from '@material-ui/core/TextField'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import { Command } from '../../../models'; +import { useDispatch } from 'react-redux'; +import { selectedCommandEditAction } from '../../../redux/plans/actions'; + +const useStyles = makeStyles( + createStyles({ + table: { + "& .MuiTableCell-root": { + padding: 8 + } + }, + nameCell: { + width: 50 + }, + valueCell: { + width: 150 + }, + typeCell: { + width: 60 + }, + valueInput: { + "& input": { + padding: 8 + } + }, +})); + +export interface SetParamAreaProps { + command: Command +} + +const SetParamTable = (props: SetParamAreaProps) => { + const classes = useStyles(); + const dispatch = useDispatch(); + const { command } = props; + + const handleExecTimeChange = (event: any) => { + const newSelectedCommand = { + ...command, + execTime: parseInt(event.target.value) + } + dispatch(selectedCommandEditAction(newSelectedCommand)); + }; + + const handleParamValueChange = (event: any, i: number) => { + const newSelectedCommand = { + ...command, + params: [ + ...command.params.slice(0,i), + { + ...command.params[i], + value: event.target.value + }, + ...command.params.slice(i+1) + ] + } + dispatch(selectedCommandEditAction(newSelectedCommand)); + }; + + return ( +
+ + Parameters + +
+ + + + + Name + Value + Type + Description + + + + {(command.execType === "TL" || command.execType === "BL" || command.execType === "UTL") && ( + + Time + + + + int32_t + TI + + )} + {command.params.length > 0 && ( + command.params.map((param,i) => + + {param.name} + + handleParamValueChange(event, i)} + value={param.value || ""} type="text" + className={classes.valueInput} + /> + + {param.type} + {param.description} + + ) + )} + +
+
+
+ ) +}; + +export default SetParamTable; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/CheckBox.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/CheckBox.tsx new file mode 100644 index 0000000..b33bf3e --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/CheckBox.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Checkbox, FormControlLabel } from "@material-ui/core" + +export interface CheckBoxProps { + label: string, + checked: boolean, + select: any +}; + +const CheckBox = (props: CheckBoxProps) => { + return ( + { props.select(event.target.checked) }}/>} + label={props.label} + /> + ) +}; + +export default CheckBox; + diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/ConfirmationDialog.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/ConfirmationDialog.tsx new file mode 100644 index 0000000..14b9773 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/ConfirmationDialog.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import Dialog from '@material-ui/core/Dialog'; + +export interface ConfirmationDialogProps { + open: boolean; + labelOk: string; + children?: React.ReactNode + onOkClick: () => void; + onClose: () => void; +} + +const ConfirmationDialog = (props: ConfirmationDialogProps) => { + const { open, labelOk, children, onOkClick, onClose } = props; + + const handleOkClick = () => { + onOkClick(); + onClose(); + } + + const handleCancelClick = () => { + onClose(); + } + + return ( + + Confirmation + + {children} + + + + + + + ) +}; + +export default ConfirmationDialog; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/EditableInputTableCell.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/EditableInputTableCell.tsx new file mode 100644 index 0000000..9cf1a9b --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/EditableInputTableCell.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import TextField from '@material-ui/core/TextField'; +import TableCell from '@material-ui/core/TableCell'; + +const useStyles = makeStyles( + createStyles({ + valueInput: { + "& input": { + padding: 8, + maxWidth: 120 + } + }, +})); + +export interface EditableInputTableCellProps { + isEditMode: boolean, + name: string, + value: string + label: string, + onChange: (event: React.ChangeEvent) => any, +}; + +const EditableInputTableCell = (props: EditableInputTableCellProps) => { + const { isEditMode, label, name, value, onChange } = props; + const classes = useStyles(); + + return ( + + {isEditMode ? ( + onChange(event)} + name={name} value={value} type="text" + className={classes.valueInput} + /> + ) : ( + label + )} + + ) +}; + +export default EditableInputTableCell; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/EditableSelectTableCell.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/EditableSelectTableCell.tsx new file mode 100644 index 0000000..c0d2323 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/EditableSelectTableCell.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import TableCell from '@material-ui/core/TableCell'; +import SelectBox, { SelectOption } from './SelectBox'; + +export interface EditableSelectTableCellProps { + isEditMode: boolean, + value: any + label: any, + options: SelectOption[], + select: any +}; + +const EditableSelectTableCell = (props: EditableSelectTableCellProps) => { + const { isEditMode, value, label, select, options } = props; + + return ( + + {isEditMode ? ( + + ) : ( + label + )} + + ) +}; + +export default EditableSelectTableCell; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/ErrorDialog.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/ErrorDialog.tsx new file mode 100644 index 0000000..d983fa2 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/ErrorDialog.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import Dialog from '@material-ui/core/Dialog'; +import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'; +import { Typography } from '@material-ui/core'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../redux/store/RootState'; +import { getErrorDialogState } from '../../redux/ui/selectors'; +import { closeErrorDialogAction } from '../../redux/ui/actions'; +import HTMLReactParser from 'html-react-parser'; + +const returnCodeToBr = (text: string) => { + if (text === "") { + return text; + } else { + return HTMLReactParser(text.replace(/\r?\n/g, '
')); + } +}; + +const ErrorDialog = () => { + const dispatch = useDispatch(); + const selector = useSelector((state: RootState) => state); + const error = getErrorDialogState(selector); + + return ( + + +
+ + エラー +
+
+ + {returnCodeToBr(error.message)} + + + + +
+ ) +}; + +export default ErrorDialog; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/FileTreeMultiView.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/FileTreeMultiView.tsx new file mode 100644 index 0000000..11037d5 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/FileTreeMultiView.tsx @@ -0,0 +1,200 @@ +import React, { useState } from 'react'; +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; +import TreeView from '@material-ui/lab/TreeView'; +import TreeItem, { TreeItemProps } from '@material-ui/lab/TreeItem'; +import Typography from '@material-ui/core/Typography'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; +import ArrowRightIcon from '@material-ui/icons/ArrowRight'; +import DescriptionIcon from '@material-ui/icons/Description'; +import DescriptionTwoToneIcon from '@material-ui/icons/DescriptionTwoTone'; +import FolderIcon from '@material-ui/icons/Folder'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; +import { FileIndex } from '../../models'; + +const useTreeItemStyles = makeStyles((theme: Theme) => + createStyles({ + content: { + color: theme.palette.text.secondary, + borderTopRightRadius: theme.spacing(2), + borderBottomRightRadius: theme.spacing(2), + paddingRight: theme.spacing(1), + fontWeight: theme.typography.fontWeightMedium, + '$expanded > &': { + fontWeight: theme.typography.fontWeightRegular, + }, + }, + group: { + marginLeft: 0, + '& $content': { + paddingLeft: theme.spacing(2), + }, + }, + expanded: {}, + selected: {}, + label: { + fontWeight: 'inherit', + color: 'inherit', + }, + labelRoot: { + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0.5, 0), + }, + labelIcon: { + marginRight: theme.spacing(1), + }, + labelText: { + fontWeight: 'inherit', + flexGrow: 1, + }, + }), +); + +interface StyledTreeItemProps extends TreeItemProps { + labelIcon: React.ElementType; + labelText: string; +} + +const StyledTreeItem = (props: StyledTreeItemProps) => { + const classes = useTreeItemStyles(); + const { labelText, labelIcon: LabelIcon, color, ...other } = props; + + return ( + + + + {labelText} + +
+ } + classes={{ + content: classes.content, + expanded: classes.expanded, + selected: classes.selected, + group: classes.group, + label: classes.label, + }} + {...other} + /> + ); +} + +const useStyles = makeStyles({ + root: { + height: 240, + flexGrow: 1, + maxWidth: 400, + }, +}); + +export interface FileTreeMultiViewProps { + files: FileIndex[], + rootPath: string, + select: any, + defaultExpandedFolder?: string[] +} + +const FileTreeMultiView = (props: FileTreeMultiViewProps) => { + const classes = useStyles(); + const { files, rootPath, select, defaultExpandedFolder } = props + + interface CheckedState { + [id: string] : boolean; + } + + let initCheckedState : CheckedState = {}; + files.forEach(element => initCheckedState[element.name] = false); + const [checkedState, setCheckedState] = useState(initCheckedState); + + const handleSelect = (event: React.ChangeEvent<{}>, nodeIds: string[]) => { + let changeCheckedState = {...checkedState}; + if (nodeIds.includes("folder--")) { + files.forEach(element => { + changeCheckedState[element.name] = false; + }) + } + else { + select(nodeIds); + files.forEach(element => { + if (nodeIds.indexOf(element.id) >= 0) { + changeCheckedState[element.name] = true; + } + else{ + changeCheckedState[element.name] = false; + } + }) + } + setCheckedState(changeCheckedState); + }; + + var tree: any = {}; + files.forEach((file: FileIndex) => { + var currentNode = tree; + const filePath = file.filePath.replace(rootPath, ""); + filePath.split('/').forEach((segment: string) => { + if (currentNode[segment] === undefined) { + if (segment.includes(".ops")) { + currentNode[file.name] = file + } else { + currentNode[segment] = {}; + } + } + currentNode = currentNode[segment]; + }) + }); + + const toTreeData = (tree: any) => { + return Object.keys(tree).map((label: any) => { + var o: any = { label: label }; + if ('id' in tree[label]) { + o.file = tree[label]; + } else if (Object.keys(tree[label]).length > 0) { + o.children = toTreeData(tree[label]); + } + return o; + }) + }; + + const showTreeItem = (data: any) => { + if (data) { + return ( + data.length > 0 && data.map((child: any) => { + const label = child.label; + if ('file' in child) { + if (checkedState[child.label] === true){ + return + } + else{ + return + } + } else { + return ( + + {showTreeItem(child.children)} + + ) + } + }) + ) + } + }; + + return ( + } + defaultExpandIcon={} + defaultExpanded={defaultExpandedFolder && defaultExpandedFolder.map(name => "folder--"+name)} + defaultEndIcon={
} + //selected={selected} + multiSelect={true} + onNodeSelect={handleSelect} + > + {showTreeItem(toTreeData(tree))} + + ) +}; + +export default FileTreeMultiView; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/FileTreeView.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/FileTreeView.tsx new file mode 100644 index 0000000..e55cb1c --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/FileTreeView.tsx @@ -0,0 +1,174 @@ +import React, { useState } from 'react'; +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; +import TreeView from '@material-ui/lab/TreeView'; +import TreeItem, { TreeItemProps } from '@material-ui/lab/TreeItem'; +import Typography from '@material-ui/core/Typography'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; +import ArrowRightIcon from '@material-ui/icons/ArrowRight'; +import DescriptionIcon from '@material-ui/icons/Description'; +import FolderIcon from '@material-ui/icons/Folder'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; +import { FileIndex } from '../../models'; + +const useTreeItemStyles = makeStyles((theme: Theme) => + createStyles({ + content: { + color: theme.palette.text.secondary, + borderTopRightRadius: theme.spacing(2), + borderBottomRightRadius: theme.spacing(2), + paddingRight: theme.spacing(1), + fontWeight: theme.typography.fontWeightMedium, + '$expanded > &': { + fontWeight: theme.typography.fontWeightRegular, + }, + }, + group: { + marginLeft: 0, + '& $content': { + paddingLeft: theme.spacing(2), + }, + }, + expanded: {}, + selected: {}, + label: { + fontWeight: 'inherit', + color: 'inherit', + }, + labelRoot: { + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0.5, 0), + }, + labelIcon: { + marginRight: theme.spacing(1), + }, + labelText: { + fontWeight: 'inherit', + flexGrow: 1, + }, + }), +); + +interface StyledTreeItemProps extends TreeItemProps { + labelIcon: React.ElementType; + labelText: string; +} + +const StyledTreeItem = (props: StyledTreeItemProps) => { + const classes = useTreeItemStyles(); + const { labelText, labelIcon: LabelIcon, color, ...other } = props; + + return ( + + + + {labelText} + +
+ } + classes={{ + content: classes.content, + expanded: classes.expanded, + selected: classes.selected, + group: classes.group, + label: classes.label, + }} + {...other} + /> + ); +} + +const useStyles = makeStyles({ + root: { + height: 240, + flexGrow: 1, + maxWidth: 400, + }, +}); + +export interface FileTreeViewProps { + files: FileIndex[], + rootPath: string, + select: any, + defaultExpandedFolder?: string[] +} + +const FileTreeView = (props: FileTreeViewProps) => { + const classes = useStyles(); + const { files, rootPath, select, defaultExpandedFolder } = props + const [selected, setSelected] = useState([]); + + const handleSelect = (event: React.ChangeEvent<{}>, nodeIds: string[]) => { + if (nodeIds.includes("folder--")) { + setSelected([]); + } else { + setSelected(nodeIds); + select(nodeIds); + } + }; + + var tree: any = {}; + files.forEach((file: FileIndex) => { + var currentNode = tree; + const filePath = file.filePath.replace(rootPath, ""); + filePath.split('/').forEach((segment: string) => { + if (currentNode[segment] === undefined) { + if (segment.includes(".ops")) { + currentNode[file.name] = file + } else { + currentNode[segment] = {}; + } + } + currentNode = currentNode[segment]; + }) + }); + + const toTreeData = (tree: any) => { + return Object.keys(tree).map((label: any) => { + var o: any = { label: label }; + if ('id' in tree[label]) { + o.file = tree[label]; + } else if (Object.keys(tree[label]).length > 0) { + o.children = toTreeData(tree[label]); + } + return o; + }) + }; + + const showTreeItem = (data: any) => { + if (data) { + return ( + data.length > 0 && data.map((child: any) => { + const label = child.label; + if ('file' in child) { + return + } else { + return ( + + {showTreeItem(child.children)} + + ) + } + }) + ) + } + }; + + return ( + } + defaultExpandIcon={} + defaultExpanded={defaultExpandedFolder && defaultExpandedFolder.map(name => "folder--"+name)} + defaultEndIcon={
} + selected={selected} + onNodeSelect={handleSelect} + > + {showTreeItem(toTreeData(tree))} + + ) +}; + +export default FileTreeView; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/IconButtonInTabs.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/IconButtonInTabs.tsx new file mode 100644 index 0000000..8a595aa --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/IconButtonInTabs.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import IconButton from '@material-ui/core/IconButton'; + +export interface IconButtonInTabsProps { + children: React.ReactNode, + className?: string | undefined, + onClick?: (event: React.MouseEvent) => void +} + +const IconButtonInTabs = (props: IconButtonInTabsProps) => { + return {props.children} +} + +export default IconButtonInTabs; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/LoadingBackDrop.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/LoadingBackDrop.tsx new file mode 100644 index 0000000..5078f8f --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/LoadingBackDrop.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import Backdrop from '@material-ui/core/Backdrop'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../redux/store/RootState'; +import { getIsLoading } from '../../redux/ui/selectors'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + backdrop: { + zIndex: theme.zIndex.drawer + 1, + color: '#fff', + }, + }), +); + +const LoadingBackDrop = () => { + const classes = useStyles(); + const selector = useSelector((state: RootState) => state); + const open = getIsLoading(selector); + + return ( +
+ + + +
+ ) +}; + +export default LoadingBackDrop; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/RadioBox.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/RadioBox.tsx new file mode 100644 index 0000000..601a9bf --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/RadioBox.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import {FormLabel, FormControl, RadioGroup, FormControlLabel, Radio, makeStyles, createStyles} from "@material-ui/core" + +export interface RadioOption { + id: string, + name: string +} + +export interface RadioBoxProps { + label: string, + value: string, + handleChange: any, + options: RadioOption[] +} + +const RadioBox = (props: RadioBoxProps) => { + return ( + + {props.label} + props.handleChange(event.target.value)} + > + {props.options.map(option => ( + } label={option.name}/> + ))} + + + ) +}; + +export default RadioBox; diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/SelectBox.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/SelectBox.tsx new file mode 100644 index 0000000..d994caf --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/SelectBox.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import {InputLabel, MenuItem, FormControl, Select, makeStyles, createStyles} from "@material-ui/core" + +const useStyles = makeStyles(createStyles({ + formControl: { + marginBottom: 16, + minWidth: 128, + width: '100%' + } +})); + +export interface SelectOption { + id: any, + name: string +}; + +export interface SelectBoxProps { + label: string, + value: any, + select: any, + options: SelectOption[] +}; + +const SelectBox = (props: SelectBoxProps) => { + const classes = useStyles(); + + return ( + + {props.label} + + + ) +}; + +export default SelectBox; + diff --git a/aspnetapp/WINGS/ClientApp/src/components/common/TransferList.tsx b/aspnetapp/WINGS/ClientApp/src/components/common/TransferList.tsx new file mode 100644 index 0000000..51e9a7b --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/common/TransferList.tsx @@ -0,0 +1,179 @@ +import React, { useEffect } from 'react'; +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; +import List from '@material-ui/core/List'; +import Card from '@material-ui/core/Card'; +import CardHeader from '@material-ui/core/CardHeader'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import Checkbox from '@material-ui/core/Checkbox'; +import Button from '@material-ui/core/Button'; +import Divider from '@material-ui/core/Divider'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + justifyContent: "left", + "& .MuiListItem-root": { + height: 30 + } + }, + cardHeader: { + padding: theme.spacing(1, 2), + }, + list: { + width: 200, + height: 230, + backgroundColor: theme.palette.background.paper, + overflow: 'auto', + }, + button: { + margin: theme.spacing(0.5, 0), + }, + }), +); + +const not = (a: string[], b: string[]) => { + return a.filter((value) => b.indexOf(value) === -1); +} + +const intersection = (a: string[], b: string[]) => { + return a.filter((value) => b.indexOf(value) !== -1); +} + +const union = (a: string[], b: string[]) => { + return [...a, ...not(b, a)]; +} + +export interface TransferListProps { + data: string[], + setSelected: (selected: string[]) => void +} + +const TransferList = (props: TransferListProps) => { + const { data, setSelected } = props; + const classes = useStyles(); + const [checked, setChecked] = React.useState([]); + const [left, setLeft] = React.useState([]); + const [right, setRight] = React.useState([]); + + const leftChecked = intersection(checked, left); + const rightChecked = intersection(checked, right); + + useEffect(() => { + setLeft(data); + }, [data]) + + const handleToggle = (value: string) => () => { + const currentIndex = checked.indexOf(value); + const newChecked = [...checked]; + + if (currentIndex === -1) { + newChecked.push(value); + } else { + newChecked.splice(currentIndex, 1); + } + + setChecked(newChecked); + }; + + const numberOfChecked = (items: string[]) => intersection(checked, items).length; + + const handleToggleAll = (items: string[]) => () => { + if (numberOfChecked(items) === items.length) { + setChecked(not(checked, items)); + } else { + setChecked(union(checked, items)); + } + }; + + const handleCheckedRight = () => { + const selected = right.concat(leftChecked); + setSelected(selected); + setRight(selected); + setLeft(not(left, leftChecked)); + setChecked(not(checked, leftChecked)); + }; + + const handleCheckedLeft = () => { + const selected = not(right, rightChecked); + setSelected(selected); + setLeft(left.concat(rightChecked)); + setRight(selected); + setChecked(not(checked, rightChecked)); + }; + + const customList = (title: React.ReactNode, items: string[]) => ( + + + } + title={title} + subheader={`${numberOfChecked(items)}/${items.length} selected`} + /> + + + {items.map((value: string) => { + const labelId = `transfer-list-all-item-${value}-label`; + + return ( + + + + + + + ); + })} + + + + ); + + return ( + + {customList('Not Selected', left)} + + + + + + + {customList('Selected', right)} + + ); +} + +export default TransferList; diff --git a/aspnetapp/WINGS/ClientApp/src/components/compo/ComponentList.tsx b/aspnetapp/WINGS/ClientApp/src/components/compo/ComponentList.tsx new file mode 100644 index 0000000..c8f6e8b --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/compo/ComponentList.tsx @@ -0,0 +1,235 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import EditIcon from '@material-ui/icons/Edit'; +import DeleteIcon from '@material-ui/icons/Delete'; +import CheckIcon from '@material-ui/icons/Check'; +import ClearIcon from '@material-ui/icons/Clear'; +import AddIcon from '@material-ui/icons/Add'; +import { Component } from '../../models'; +import { openErrorDialogAction } from '../../redux/ui/actions'; +import ConfirmationDialog from '../common/ConfirmationDialog'; +import EditableInputTableCell from '../common/EditableInputTableCell'; +import EditableSelectTableCell from '../common/EditableSelectTableCell'; + +const initialValues = { + name: "", + localDirPath: "", + tcPacketKey: "", + tmPacketKey: "" +}; + +export interface ComponentListProps { + compos: Component[], + updateState: () => void + } + +const ComponentList = (props: ComponentListProps) => { + const { compos, updateState } = props; + const dispatch = useDispatch(); + const [open, setOpen] = useState(false); + const [deleteCompo, setDeleteCompo] = useState(null); + const [isEditModeArr, setIsEditModeArr ] = useState([]); + const [isAddMode, setIsAddMode] = useState(false); + const [values, setValues] = useState(initialValues); + + useEffect(() => { + setIsEditModeArr(Array(compos.length).fill(false)) + }, [compos]) + + const handleDeleteClick = (compo: Component) => { + setOpen(true); + setDeleteCompo(compo); + } + + const handleOkClick = async () => { + await fetch(`/api/components/${deleteCompo?.id}`, { + method: 'DELETE' + }) + updateState(); + } + + const handleDialogClose = () => { + setOpen(false); + setDeleteCompo(null); + } + + const toggleEditMode = (i: number) => { + const newArr = Array(compos.length).fill(false); + newArr.splice(i,1,!isEditModeArr[i]); + setIsEditModeArr(newArr); + setValues({ + name: compos[i].name, + localDirPath: compos[i].localDirPath, + tcPacketKey: compos[i].tcPacketKey, + tmPacketKey: compos[i].tmPacketKey + }) + }; + + const handleChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + setValues({ ...values, [name]: value}); + }; + + const updateComponent = async (i: number) => { + const compo = compos[i]; + const res = await fetch(`/api/components/${compo.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: compo.id, + name: values.name, + localDirPath: values.localDirPath, + tcPacketKey: values.tcPacketKey, + tmPacketKey: values.tmPacketKey + }) + }); + if (res.status === 200) { + toggleEditMode(i); + updateState(); + } else { + const json = await res.json(); + const message = `Status Code: ${res.status}\n${json.message ? json.message: "unknown error"}`; + dispatch(openErrorDialogAction(message)); + } + }; + + const createComponent = async () => { + const res = await fetch(`/api/components`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify({ + name: values.name, + localDirPath: values.localDirPath, + tcPacketKey: values.tcPacketKey, + tmPacketKey: values.tmPacketKey + }) + }); + if (res.status === 201) { + toggleAddMode(); + updateState(); + } else { + const json = await res.json(); + const message = `Status Code: ${res.status}\n${json.message ? json.message: "unknown error"}`; + dispatch(openErrorDialogAction(message)); + } + } + + const toggleAddMode = () => { + setIsAddMode(!isAddMode); + setIsEditModeArr(Array(compos.length).fill(false)); + setValues(initialValues); + }; + + return ( + <> +
+

Components List

+ + + + + +
+
+ + + + + Name + LocalDirPath + TcPacketKey + TmPacketKey + + + + + + {compos.length > 0 && ( + compos.map((compo,i) => ( + + + + + + + {isEditModeArr[i] ? ( + <> + + updateComponent(i)}> + + + + + toggleEditMode(i)}> + + + + + ) : ( + + toggleEditMode(i)}> + + + + )} + + + + handleDeleteClick(compo)}> + + + + + + )) + )} + {isAddMode && ( + + + + + + + + + + + + + + + + + + + + )} + +
+
+
+ await handleOkClick()} + labelOk="Delete" onClose={handleDialogClose} + > +

Are you sure to delete

+

{deleteCompo?.name}

+

?

+
+ + ) +}; + +export default ComponentList; diff --git a/aspnetapp/WINGS/ClientApp/src/components/compo/ComponentManage.tsx b/aspnetapp/WINGS/ClientApp/src/components/compo/ComponentManage.tsx new file mode 100644 index 0000000..d6f87bd --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/compo/ComponentManage.tsx @@ -0,0 +1,31 @@ +import React, {useEffect, useState } from 'react'; +import { Component } from '../../models'; +import ComponentList from './ComponentList'; + +const ComponentManage = () => { + const [compos, setCompos] = useState([]); + + const fetchData = async () => { + const response_compo = await fetch('/api/components', { + method: 'GET' + }); + if (response_compo.status == 200) { + const json_compo = await response_compo.json(); + const data_compo = json_compo.data as Component[]; + setCompos(data_compo); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + return ( +
+ +
+
+ ); +} + +export default ComponentManage; diff --git a/aspnetapp/WINGS/ClientApp/src/components/header/DrawerMenus.tsx b/aspnetapp/WINGS/ClientApp/src/components/header/DrawerMenus.tsx new file mode 100644 index 0000000..16aeb39 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/header/DrawerMenus.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { createStyles, makeStyles } from '@material-ui/core'; +import { useDispatch } from 'react-redux'; +import { push } from 'connected-react-router'; +import Drawer from '@material-ui/core/Drawer'; +import IconButton from '@material-ui/core/IconButton'; +import HomeIcon from '@material-ui/icons/Home'; +import HistoryIcon from '@material-ui/icons/History'; +import SettingsIcon from '@material-ui/icons/Settings'; +import ComputerIcon from '@material-ui/icons/Computer'; +import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import Divider from '@material-ui/core/Divider'; + +const useStyles = makeStyles( + createStyles({ + drawer: { + flexShrink: 0, + width: 256 + }, + drawerPaper: { + width: 256 + } +})); + +export interface DrawerMenusProps { + open: boolean, + onClose: (event: {}) => void +} + +const DrawerMenus = (props: DrawerMenusProps) => { + const classes = useStyles(); + const dispatch = useDispatch(); + + const selectMenu = (event: {}, path: string) => { + dispatch(push(path)); + props.onClose(event); + } + + const menus = [ + {id: "home", label: "Home", icon: , value: "/"}, + {id: "history", label: "Operation History", icon: , value: "/history"}, + {id: "compo", label: "Components", icon: , value: "/settings/components"}, + {id: "setting", label: "Settings", icon: , value: "/settings"} + ]; + + return ( + + ) +}; + +export default DrawerMenus; diff --git a/aspnetapp/WINGS/ClientApp/src/components/header/Header.tsx b/aspnetapp/WINGS/ClientApp/src/components/header/Header.tsx new file mode 100644 index 0000000..297d4c3 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/header/Header.tsx @@ -0,0 +1,46 @@ +import React, { useCallback, useState } from 'react'; +import {createStyles, makeStyles } from '@material-ui/styles' +import { Theme } from '@material-ui/core' +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import HeaderMenus from './HeaderMenus'; +import DrawerMenus from './DrawerMenus'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + flexGrow: 1, + }, + appBar: { + backgroundColor: theme.palette.grey[800] + }, + toolBar: { + paddingLeft: 5 + } +})); + +const Header = () => { + const classes = useStyles(); + + const [open, setOpen] = useState(false); + + const handleDrawerToggle = useCallback((event) => { + if (event.type === 'keydown' && (event.key === 'Tab' || event.key ==='Shift')) { + return; + } + setOpen(!open); + }, [setOpen, open]) + + return ( +
+ + + + + + +
+ ); +}; + +export default Header; diff --git a/aspnetapp/WINGS/ClientApp/src/components/header/HeaderMenus.tsx b/aspnetapp/WINGS/ClientApp/src/components/header/HeaderMenus.tsx new file mode 100644 index 0000000..82f21f1 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/header/HeaderMenus.tsx @@ -0,0 +1,67 @@ +import React, { useState, useEffect, useRef } from 'react'; +import {createStyles, makeStyles} from '@material-ui/styles' +import IconButton from '@material-ui/core/IconButton'; +import Button from '@material-ui/core/Button'; +import MenuIcon from '@material-ui/icons/Menu'; +import { useDispatch, useSelector } from 'react-redux'; +import logo from '../../assets/img/logo.png'; +import { push } from 'connected-react-router'; +import { getOpid } from '../../redux/operations/selectors'; +import { RootState } from '../../redux/store/RootState'; +import { updateCommandLogAction } from '../../redux/commands/actions'; + +const useStyles = makeStyles( + createStyles({ + grow: { + flexGrow: 1 + }, + menuButton: { + fontSize: 16 + } +})); + +export interface HeaderMenusProps { + handleDrawerToggle: (event: React.MouseEvent) => void +} + +const HeaderMenus = (props: HeaderMenusProps) => { + const classes = useStyles(); + const dispatch = useDispatch(); + const selector = useSelector((state: RootState) => state); + const opid = getOpid(selector); + + const handleCmdLog = async () => { + dispatch(push('/command_log')); + if (opid === "") return; + const res = await fetch(`/api/operations/${opid}/cmd_fileline/log`, { + method: 'GET' + }); + const json = await res.json(); + const data = json.data; + dispatch(updateCommandLogAction(data)); + }; + + return ( + <> + props.handleDrawerToggle(event)}> + + + Logo dispatch(push('/'))} + /> + + + +
+ + ) +}; + +export default HeaderMenus; diff --git a/aspnetapp/WINGS/ClientApp/src/components/history/HistoryDetail.tsx b/aspnetapp/WINGS/ClientApp/src/components/history/HistoryDetail.tsx new file mode 100644 index 0000000..e1354db --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/history/HistoryDetail.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableRow from '@material-ui/core/TableRow'; +import { Operation, LocationState } from '../../models'; +import LogExportArea from './LogExportArea'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: '100%', + maxWidth: 1000, + backgroundColor: theme.palette.background.paper, + } + }) +); + +const HistoryDetail = () => { + const classes = useStyles(); + const location = useLocation>(); + const operation = location.state.data; + + return ( +
+

Details

+
+ + + + + opid + {operation.id} + + + Path Number + {operation.pathNumber} + + + Comment + {operation.comment} + + + Component + {operation.component.name} + + + Operator + {operation.operator ? operation.operator.userName : "Unkown"} + + + Start Time + {operation.createdAt.replace("T", " ")} + + +
+
+
+
+ +
+ ); +}; + +export default HistoryDetail diff --git a/aspnetapp/WINGS/ClientApp/src/components/history/LogExportArea.tsx b/aspnetapp/WINGS/ClientApp/src/components/history/LogExportArea.tsx new file mode 100644 index 0000000..ff0b241 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/history/LogExportArea.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useState } from 'react'; +import IconButton from '@material-ui/core/IconButton'; +import GetAppIcon from '@material-ui/icons/GetApp'; +import TransferList from '../common/TransferList'; +import { saveAs } from 'file-saver'; + +const extractFileName = (contentDispositionValue: any) => { + var filename = ""; + if (contentDispositionValue && contentDispositionValue.indexOf('attachment') !== -1) { + var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; + var matches = filenameRegex.exec(contentDispositionValue); + if (matches != null && matches[1]) { + filename = matches[1].replace(/['"]/g, ''); + } + } + return filename; +} + +export interface LogExportAreaProps { + opid: string +} + +const LogExportArea = (props: LogExportAreaProps) => { + const { opid } = props; + const [packets, setPackets] = useState([]); + const [selectedPackets, setSelectedPackets] = useState([]); + const [recordPackets, setRecordPackets] = useState([]); + const [selectedRecordPackets, setSelectedRecordPackets] = useState([]); + + const fetchTlmPackets = async () => { + const response = await fetch(`/api/operations/${opid}/history/tlm_packets`, { + method: 'GET' + }); + const json = await response.json(); + const data: string[] = json.data; + setPackets(data); + } + const fetchRecordTlmPackets = async () => { + const response = await fetch(`/api/operations/${opid}/history/record_tlm_packets`, { + method: 'GET' + }); + const json = await response.json(); + const data: string[] = json.data; + setRecordPackets(data); + } + + const downloadCommandLog = async () => { + const options = { + method: 'GET', + headers: { + 'Accept': 'text/csv' + }, + responseType: 'blob' + }; + const response = await fetch(`/api/operations/${opid}/history/cmd_logs`, options); + const contentDisposition = response.headers.get("content-disposition"); + const fileName = extractFileName(contentDisposition); + const blob = await response.blob(); + saveAs(blob, fileName); + }; + + const downloadCommandFileLog = async () => { + const options = { + method: 'GET', + headers: { + 'Accept': 'text/csv' + }, responseType: 'blob' + }; + const response = await fetch(`/api/operations/${opid}/history/cmdfile_logs`, options); + const contentDisposition = response.headers.get("content-disposition"); + const fileName = extractFileName(contentDisposition); + const blob = await response.blob(); + saveAs(blob, fileName); + }; + + const downloadTelemetryLog = async () => { + const packetNames = selectedPackets.join(); + const options = { + method: 'GET', + headers: { + 'Accept': 'application/zip' + }, responseType: 'blob' + }; + const response = await fetch(`/api/operations/${opid}/history/tlm_logs?packet_name=${packetNames}`, options); + const contentDisposition = response.headers.get("content-disposition"); + const fileName = extractFileName(contentDisposition); + const blob = await response.blob(); + saveAs(blob, fileName); + } + + const downloadRecordTelemetryLog = async () => { + const packetNames = selectedRecordPackets.join(); + const options = { + method: 'GET', + headers: { + 'Accept': 'application/zip' + }, + responseType: 'blob' + }; + const response = await fetch(`/api/operations/${opid}/history/record_tlm_logs?packet_name=${packetNames}`, options); + const contentDisposition = response.headers.get("content-disposition"); + const fileName = extractFileName(contentDisposition); + const blob = await response.blob(); + saveAs(blob, fileName); + } + + useEffect(() => { + fetchTlmPackets(); + }, []) + useEffect(() => { + fetchRecordTlmPackets(); + }, []) + + return ( + <> +

Export

+
+
+ + + + Command Logs +
+
+ + + + Command File Logs +
+
+ + + + Telemetry Logs +
+
+ +
+
+ + + + DR Telemetry Logs +
+
+ +
+ + ) +}; + +export default LogExportArea; diff --git a/aspnetapp/WINGS/ClientApp/src/components/history/OperationHistory.tsx b/aspnetapp/WINGS/ClientApp/src/components/history/OperationHistory.tsx new file mode 100644 index 0000000..0cc1130 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/history/OperationHistory.tsx @@ -0,0 +1,174 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import { useDispatch } from 'react-redux'; +import { push } from 'connected-react-router'; +import Tooltip from '@material-ui/core/Tooltip'; +import IconButton from '@material-ui/core/IconButton'; +import TextField from '@material-ui/core/TextField'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import Pagination from '@material-ui/lab/Pagination'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import DeleteIcon from '@material-ui/icons/Delete'; +import SearchIcon from '@material-ui/icons/Search'; +import { Operation, PaginationMeta } from '../../models'; +import ConfirmationDialog from '../common/ConfirmationDialog'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + paginationRoot: { + '& > *': { + marginTop: theme.spacing(2), + } + }, + searchField: { + "& input": { + padding: 8 + } + } + }) +); + +const sizePerPage = 12; + +const OperationHistory = () => { + const classes = useStyles(); + const dispatch = useDispatch(); + + const [meta, setMeta] = useState(); + const [operations, setOperations] = useState([]); + const [search, setSearch] = useState(""); + const [page, setPage] = useState(1); + const [open, setOpen] = useState(false); + const [deleteOperation, setDeleteOperation] = useState(null); + + const inputSearch = useCallback((event) => { + setSearch(event.target.value) + }, [setSearch]); + + const fetchOperations = async () => { + const response = await fetch(`/api/operations/history?page=${page}&size=${sizePerPage}&search=${search}`, { + method: 'GET' + }); + if (response.status == 200) { + const json = await response.json(); + const meta = json.meta as PaginationMeta; + const data = json.data as Operation[]; + setMeta(meta); + setOperations(data); + } + } + + const handlePageChange = (event: React.ChangeEvent, value: number) => { + setPage(value); + }; + + const handleDetailClick = (operation: Operation) => { + dispatch(push(`/history/${operation.id}`, {data: operation})) + } + + const confirmSearch = () => { + setPage(1); + fetchOperations(); + } + + const handelDeleteClick = (operation: Operation) => { + setOpen(true); + setDeleteOperation(operation); + } + + const handleOkClick = async () => { + await fetch(`/api/operations/${deleteOperation?.id}/history`, { + method: 'DELETE' + }); + fetchOperations(); + } + + const handleDialogClose = () => { + setOpen(false); + setDeleteOperation(null); + } + + useEffect(() => { + fetchOperations(); + }, [page]); + + return ( +
+

Operation History

+
+
+
+ +
+ {e.key === "Enter" && confirmSearch()}} + /> + + + +
+
+ + + + + Path Number + Comment + Component + + + + + + {operations.length > 0 && ( + operations.map(operation => ( + + {operation.pathNumber} + {operation.comment} + {operation.component && operation.component.name} + + + handleDetailClick(operation)}> + + + + + + + handelDeleteClick(operation)}> + + + + + + )) + )} + +
+
+
+ await handleOkClick()} + labelOk="Delete" onClose={handleDialogClose} + > +

Are you sure to delete log of

+

{deleteOperation?.pathNumber} : {deleteOperation?.comment}

+

+
+
+ ) +}; + +export default OperationHistory; diff --git a/aspnetapp/WINGS/ClientApp/src/components/home/Home.tsx b/aspnetapp/WINGS/ClientApp/src/components/home/Home.tsx new file mode 100644 index 0000000..fb0e6ef --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/home/Home.tsx @@ -0,0 +1,51 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import OperationList from './OperationList'; +import StartOperationArea from './StartOperationArea'; +import { Operation } from '../../models'; +import { RootState } from '../../redux/store/RootState'; +import { getOpid } from '../../redux/operations/selectors'; +import { leaveOperationAction } from '../../redux/operations/actions'; + +const Home = () => { + const dispatch = useDispatch(); + const selector = useSelector((state: RootState) => state); + const opid = getOpid(selector); + + const [operations, setOperations] = useState([]); + + const fetchOperations = async () => { + const res = await fetch('/api/operations', { + method: 'GET' + }); + if (res.status == 200) { + const json = await res.json(); + const data = json.data as Operation[]; + if (opid !== "" && !data.some(operation => operation.id === opid)) { + dispatch(leaveOperationAction()); + } + const sortedData = data.sort((a, b) => { + if (a.pathNumber < b.pathNumber) return 1; + if (a.pathNumber > b.pathNumber) return -1; + return 0; + }); + setOperations(sortedData); + } + } + + useEffect(() => { + fetchOperations(); + }, []); + + return ( +
+

Operation Lists

+ +
+

Start New Operation

+ +
+ ) +}; + +export default Home; diff --git a/aspnetapp/WINGS/ClientApp/src/components/home/OperationList.tsx b/aspnetapp/WINGS/ClientApp/src/components/home/OperationList.tsx new file mode 100644 index 0000000..f586c7a --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/home/OperationList.tsx @@ -0,0 +1,136 @@ +import React, { useState } from 'react'; +import { makeStyles, createStyles } from '@material-ui/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import InputIcon from '@material-ui/icons/Input'; +import StopIcon from '@material-ui/icons/Stop'; +import UndoIcon from '@material-ui/icons/Undo'; +import { useDispatch, useSelector } from 'react-redux'; +import { getOpid } from '../../redux/operations/selectors'; +import { leaveOperationAction } from '../../redux/operations/actions'; +import { joinOperation } from '../../redux/operations/operations'; +import { Operation } from '../../models'; +import { RootState } from '../../redux/store/RootState'; +import { cyan } from "@material-ui/core/colors"; +import ConfirmationDialog from '../common/ConfirmationDialog'; +import { openErrorDialogAction } from '../../redux/ui/actions'; + +const useStyles = makeStyles( + createStyles({ + isJoining: { + backgroundColor: cyan[800] + }, + }) +); + +export interface OperationListProps { + operations: Operation[], + updateState: () => void +} + +const OperationList = (props: OperationListProps) => { + const classes = useStyles(); + const selector = useSelector((state: RootState) => state); + const dispatch = useDispatch(); + const opid = getOpid(selector); + + const [open, setOpen] = useState(false); + const [stopOperation, setStopOperation] = useState(null); + + const handleStopClick = (operation: Operation) => { + setOpen(true); + setStopOperation(operation); + } + + const handleOkClick = async () => { + const res = await fetch(`/api/operations/${stopOperation?.id}`, { + method: 'DELETE' + }) + if (res.status === 204) { + if (stopOperation?.id === opid) { + dispatch(leaveOperationAction()); + } + props.updateState(); + } else if (res.status === 403) { + const message = `Status Code: ${res.status}\nYou don't have the necessary authority`; + dispatch(openErrorDialogAction(message)); + } + } + + const handleDialogClose = () => { + setOpen(false); + setStopOperation(null); + } + + const leaveOperation = (id: string) => { + id === opid && dispatch(leaveOperationAction()); + } + + return ( +
+ + + + + Path Number + Comment + Component + + + + + + + {props.operations.length > 0 && ( + props.operations.map(operation => ( + + {operation.pathNumber} + {operation.comment} + {operation.component.name} + + + dispatch(joinOperation(operation))}> + + + + + + + leaveOperation(operation.id)}> + + + + + + + handleStopClick(operation)}> + + + + + + )) + )} + +
+
+ await handleOkClick()} + labelOk="Stop" onClose={handleDialogClose} + > +

Are your sure to stop

+

{stopOperation?.pathNumber} : {stopOperation?.comment}

+

?

+
+
+ ); +}; + +export default OperationList; diff --git a/aspnetapp/WINGS/ClientApp/src/components/home/StartOperationArea.tsx b/aspnetapp/WINGS/ClientApp/src/components/home/StartOperationArea.tsx new file mode 100644 index 0000000..1a0a71d --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/home/StartOperationArea.tsx @@ -0,0 +1,147 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { Button, TextField, makeStyles, createStyles } from '@material-ui/core'; +import SelectBox, { SelectOption } from '../common/SelectBox'; +import RadioBox from '../common/RadioBox'; +import { Component } from '../../models'; +import { useDispatch } from 'react-redux'; +import { openErrorDialogAction, startLoadingAction, endLoadingAction } from '../../redux/ui/actions'; + +const useStyles = makeStyles( + createStyles({ + container: { + maxWidth: 700 + }, + button: { + width: 120 + } +})); + +const getDefaultPathNumber = () => { + const dt = new Date(); + const YY = ('00' + dt.getFullYear()).slice(-2); + const MM = ('00' + (dt.getMonth()+1)).slice(-2); + const DD = ('00' + dt.getDate()).slice(-2); + const hh = ('00' + dt.getHours()).slice(-2); + const mm = ('00' + dt.getMinutes()).slice(-2); + return (YY + MM + DD + "-" + hh + mm); +} + +const fileLocationOptions = [ + {id: "Local", name: "Local"} +] + +const tmtcTargetOptions = [ + { id: "TmtcIf", name: "TmtcIf" } +] + +export interface StartOperationAreaProps { + updateState: () => void +} + +const StartOperationArea = (props: StartOperationAreaProps) => { + const classes = useStyles(); + const dispatch = useDispatch(); + + const [pathNumber, setPathNumber] = useState(getDefaultPathNumber()), + [comment, setComment] = useState(""), + [compoId, setCompoId] = useState(""), + [compos, setCompos] = useState([]), + [fileLocation, setFileLocation] = useState("Local"), + [tmtcTarget, setTmtcTarget] = useState("TmtcIf") + + const inputPathNumber = useCallback((event) => { + setPathNumber(event.target.value) + },[setPathNumber]); + + const inputComment = useCallback((event) => { + setComment(event.target.value) + },[setComment]); + + const fetchComponents = async () => { + const res = await fetch('/api/components', { + method: 'GET' + }); + if (res.status == 200) { + const json = await res.json(); + const data = json.data as Component[]; + setCompos(data); + } + } + + const startOperation = async () => { + dispatch(startLoadingAction()); + const res = await fetch(`/api/operations`,{ + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + operation: { + pathNumber: pathNumber, + comment: comment, + fileLocation: fileLocation, + tmtcTarget: tmtcTarget, + componentId: compoId + }, + }, ["operation", "pathNumber", "comment", "fileLocation", "tmtcTarget", "componentId"]) + }); + dispatch(endLoadingAction()); + if (res.status === 201) { + setPathNumber(getDefaultPathNumber()); + setComment(""); + props.updateState(); + } else { + const json = await res.json(); + const message = `Status Code: ${res.status}\n${json.message ? json.message: "unknown error"}`; + dispatch(openErrorDialogAction(message)); + } + } + + useEffect(() => { + fetchComponents(); + }, []); + + return ( + <> +
+
+ +
+ +
+ +
+ +
+ + {fileLocation === "Local" && ( +

{compos.find(compo => compo.id === compoId)?.localDirPath}

+ )} + + +
+ + ) +}; + +export default StartOperationArea; diff --git a/aspnetapp/WINGS/ClientApp/src/components/telemetry/TelemetryViewer.tsx b/aspnetapp/WINGS/ClientApp/src/components/telemetry/TelemetryViewer.tsx new file mode 100644 index 0000000..2ae2a69 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/telemetry/TelemetryViewer.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import { useDispatch,useSelector } from 'react-redux'; +import { RootState } from '../../redux/store/RootState'; +import { getBlockInfos } from '../../redux/views/selectors'; +import ViewDisplayBlock from './view_display/ViewDisplayBlock'; +import { Button } from '@material-ui/core'; +import { useState } from 'react'; +import { getOpid } from '../../redux/operations/selectors'; +import OpenLayoutDialog from './view_display/OpenLayoutDialog'; + +const useStyles = makeStyles( + createStyles({ + root: { + display: "flex", + flexWrap: "wrap" + }, + button: { + width: 120 + }, + dialogPaper: { + width: '80%', + maxHeight: 435, + } +})); + +const TelemetryViewer = () => { + const classes = useStyles(); + const selector = useSelector((state: RootState) => state); + const blockInfos = getBlockInfos(selector); + const [dialogOpen, setDialogOpen] = useState(false); + + const handleDialogOpen = () => { + setDialogOpen(true); + } + const handleDialogClose = () => { + setDialogOpen(false); + }; + + const opid = getOpid(selector); + + return ( +
+
+ +
+
+ {blockInfos.length && ( + blockInfos.map((blockInfo,i) => ( + + )) + )} +
+
+ +
+
+ ) +}; + +export default TelemetryViewer; \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/GraphTabPanel.tsx b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/GraphTabPanel.tsx new file mode 100644 index 0000000..f97bbcf --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/GraphTabPanel.tsx @@ -0,0 +1,305 @@ +import React from 'react'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import { useSelector, useDispatch } from 'react-redux'; +import { TelemetryViewIndex } from '../../../models'; +import { RootState } from '../../../redux/store/RootState'; +import { getTelemetryHistories } from '../../../redux/telemetries/selectors'; +import { Line } from 'react-chartjs-2'; +import Radio from '@material-ui/core/Radio'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import FormControl from '@material-ui/core/FormControl'; +import FormLabel from '@material-ui/core/FormLabel'; +import CenterFocusStrongIcon from '@material-ui/icons/CenterFocusStrong'; +import IconButtonInTabs from '../../common/IconButtonInTabs'; +import OpenGraphTabDialog from './OpenGraphTabDialog'; +import Toolbar from '@material-ui/core/Toolbar'; +import { TextField } from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import { setTelemetryTypeGraphAction } from '../../../redux/views/actions'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import { Dialog } from '@material-ui/core'; + + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + padding: 10 + }, + item: { + [theme.breakpoints.down('sm')]: { + width: '100%' + }, + [theme.breakpoints.up('sm')]: { + width: '50%' + }, + [theme.breakpoints.up('md')]: { + width: '33.33%' + }, + }, + tlmul: { + paddingInlineStart: 0, + margin: 0 + }, + tlmli: { + fontSize: 'xx-small', + display: 'block', + "& span" : { + color: theme.palette.success.main + } + }, + dialogPaper: { + width: '80%', + maxHeight: 435, + }, + dataTypeField: { + fontSize: "10pt", + textAlign:"center" + }, + inputData: { + width: "3cm", + fontSize: "10pt" + }, +})); + +export interface GraphTabPanelProps { + tab: TelemetryViewIndex, + blockNum: number +} + +const GraphTabPanel = (props: GraphTabPanelProps) => { + const { tab, blockNum } = props; + const selector = useSelector((state: RootState) => state); + const classes = useStyles(); + const dispatch = useDispatch(); + const telemetryHistories = getTelemetryHistories(selector)[tab.name]; + + const [dataType, setDataType] = React.useState(tab.dataType); + const [dataLength, setDataLength] = React.useState(tab.dataLength); + const [ylabelMin, setYlabelMin] = React.useState(tab.ylabelMin); + const [ylabelMax, setYlabelMax] = React.useState(tab.ylabelMax); + + const [showModal, setShowModal] = React.useState(false); + + + const handleChange = (event: React.ChangeEvent) => { + setDataType((event.target as HTMLInputElement).value); + }; + + const [dialogOpen, setDialogOpen] = React.useState(false); + const graphColor : {backgroundColor: string; borderColor: string;}[] = [ + {backgroundColor: "#008080", borderColor: "#7fffd4"}, + {backgroundColor: "#1e90ff", borderColor: "#87cefa"}, + {backgroundColor: "#b8860b", borderColor: "#ffd700"}, + {backgroundColor: "#c71585", borderColor: "#ee82ee"}, + {backgroundColor: "#483d8b", borderColor: "#8a2be2"}, + {backgroundColor: "#800000", borderColor: "#b22222"}, + {backgroundColor: "#f65200", borderColor: "#ff9966"}, + {backgroundColor: "#808080", borderColor: "#dcdcdc"}, + {backgroundColor: "#0000cd", borderColor: "#1e90ff"}, + {backgroundColor: "#ff8989", borderColor: "#ff4343"}, + ]; + + let tlmData: {[id: string] : number[]} = {}; + let tlmLabels: {[id: string] : string[]} = {}; + const items: { label: string; backgroundColor: string; borderColor: string; pointBorderWidth: number; data: number[]; }[] = []; + + const inputDataLength = React.useCallback((event) => { + setDataLength(event.target.value) + },[setDataLength]); + + const inputYlabelMin = React.useCallback((event) => { + if (Number(event.target.value) != NaN){ + setYlabelMin(event.target.value); + } + else{ + setYlabelMin(''); + } + },[setYlabelMin]); + + const inputYlabelMax = React.useCallback((event) => { + if (Number(event.target.value) != NaN){ + setYlabelMax(event.target.value); + } + else{ + setYlabelMax(''); + } + },[setYlabelMax]); + + let maxValue:number = 0; + let minValue:number = 0; + let isFirstValueSet = false; + + tab.selectedTelemetries.forEach((telemetryName,index) =>{ + const selectedTelemetryHistory = telemetryHistories.find(element => element.telemetryInfo.name == telemetryName); + if (selectedTelemetryHistory != undefined) { + let tlmDataTmp: number[] = []; + let tlmLabelTmp: string[] = []; + selectedTelemetryHistory.telemetryValues.forEach(tlm => { + if (tab.dataType != "Raw"){ + tlmDataTmp.push(Number(tlm.value)); + } + else{ + tlmDataTmp.push(Number(tlm.rawValue)); + } + tlmLabelTmp.push(tlm.time); + }) + + if ((Number(tab.dataLength) !== 0 || NaN) && (tlmDataTmp.length >= Number(tab.dataLength)) ) { + tlmData[telemetryName] = tlmDataTmp.slice(tlmDataTmp.length-Number(tab.dataLength), tlmDataTmp.length); + tlmLabels[telemetryName] = tlmLabelTmp.slice(tlmDataTmp.length-Number(tab.dataLength),tlmDataTmp.length); + } + else{ + tlmData[telemetryName] = tlmDataTmp; + tlmLabels[telemetryName] = tlmLabelTmp; + } + + if(!isFirstValueSet && !isNaN(tlmData[telemetryName][0])){ + maxValue = tlmData[telemetryName].reduce((num1, num2) => Math.max(num1, num2)); + minValue = tlmData[telemetryName].reduce((num1, num2) => Math.min(num1, num2)); + isFirstValueSet = true; + } + else if(!isNaN(tlmDataTmp[0])){ + let tempMaxValue = tlmData[telemetryName].reduce((num1: number, num2: number) => Math.max(num1, num2)); + let tempMinValue = tlmData[telemetryName].reduce((num1: number, num2: number) => Math.min(num1, num2)); + maxValue = Math.max(maxValue,tempMaxValue); + minValue = Math.min(minValue,tempMinValue); + } + } + + items.push( + { + label: telemetryName, + backgroundColor: graphColor[index%graphColor.length].backgroundColor, + borderColor: graphColor[index%graphColor.length].borderColor, + pointBorderWidth: 1, + data: tlmData[telemetryName] + }) + }); + + const handleDialogOpen = () => { + setDialogOpen(true); + } + + const handleDialogClose = () => { + setDialogOpen(false); + }; + + const handleOk = () => { + if ( + isNaN(Number(ylabelMin)) + || isNaN(Number(ylabelMax)) + || ( + Number(ylabelMin) != 0 + && ( + (Number(ylabelMax) == 0 && Number(ylabelMin) >= maxValue) + || (Number(ylabelMax) != 0 && Number(ylabelMin) >= Number(ylabelMax)) + ) + ) + || ( + Number(ylabelMax) != 0 + && (Number(ylabelMin) == 0 && Number(ylabelMax) <= minValue) + ) + ){ + setShowModal(true); + } + else{ + dispatch(setTelemetryTypeGraphAction(blockNum,dataType,dataLength,ylabelMin,ylabelMax)); + } + }; + + const data = { + labels: tlmLabels[tab.selectedTelemetries[0]], + datasets: items + } + + const options = { + animation:false, + scales: { + xAxes: [{ + type: "time", + time: { + parser: 'YYYY-MM-DD HH:mm:ss.S', + unit: 'minute', + stepSize: 1, + } + }], + yAxes: { + min: (tab.ylabelMin!='')?Number(tab.ylabelMin):null, + max: (tab.ylabelMax!='')?Number(tab.ylabelMax):null + } + } + } + + return ( +
+ + + + + + Data Type + + + } label="Default" /> + } label="Raw" /> + + + + + + + + + + + + Error + +

Error in y axis settings

+
+ + + +
+
+ ) +}; + +export default GraphTabPanel; diff --git a/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenGraphTabDialog.tsx b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenGraphTabDialog.tsx new file mode 100644 index 0000000..9081112 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenGraphTabDialog.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import Dialog from '@material-ui/core/Dialog'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormGroup from '@material-ui/core/FormGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../../redux/store/RootState'; +import { SelectOption } from '../../common/SelectBox'; +import { TelemetryViewIndex } from '../../../models'; +import { getTelemetryHistories } from '../../../redux/telemetries/selectors'; +import { selectTelemetryAction } from '../../../redux/views/actions'; + +const useStyles = makeStyles( + createStyles({ + paper: { + height: '80vh', + width: 500 + } +})); + +export interface OpenGraphTabDialogProps { + blockNum: number, + classes: Record<'paper', string>; + keepMounted: boolean; + open: boolean; + tab: TelemetryViewIndex; + onClose: () => void; +} + +const OpenGraphTabDialog = (props: OpenGraphTabDialogProps) => { + const {tab, onClose, blockNum, open } = props; + const classes = useStyles(); + const selector = useSelector((state: RootState) => state); + const dispatch = useDispatch(); + const formGroupRef = React.useRef(null); + + const telemetryHistories = getTelemetryHistories(selector)[tab.name]; + const telemetryOptions: SelectOption[] = telemetryHistories.map(telemetryHistory => ({ id: telemetryHistory.telemetryInfo.name, name: telemetryHistory.telemetryInfo.name })); + + + interface CheckboxState { + [id: string] : boolean; + } + + let initCheckboxState : CheckboxState = {}; + telemetryOptions.forEach(telemetryOption => { + if (tab.selectedTelemetries.includes(telemetryOption.name) == true){ + initCheckboxState[telemetryOption.id] = true; + } + else{ + initCheckboxState[telemetryOption.id] = false; + } + }); + + const [checkboxState, setCheckboxState] = React.useState(initCheckboxState); + + const handleEntering = () => { + if(formGroupRef.current != null){ + formGroupRef.current.focus(); + } + }; + + const handleCancel = () => { + onClose(); + setCheckboxState(initCheckboxState); + }; + + const handleOk = () => { + let telemetryActive :string[] = []; + telemetryOptions.forEach(telemetryOption => { + if (checkboxState[telemetryOption.id] === true){ + telemetryActive.push(telemetryOption.name); + } + }); + dispatch(selectTelemetryAction(blockNum, telemetryActive)); + onClose(); + }; + + const handleChange = (event: React.ChangeEvent) => { + setCheckboxState({...checkboxState, [(event.target as HTMLInputElement).value]:event.target.checked}); + }; + + return ( + + + + {telemetryOptions.length > 0 && ( + telemetryOptions.map(telemetryOption => { + return } + label={telemetryOption.name} + /> + }) + )} + + + + + + + + ) +}; + +export default OpenGraphTabDialog; \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenLayoutDialog.tsx b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenLayoutDialog.tsx new file mode 100644 index 0000000..374615a --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenLayoutDialog.tsx @@ -0,0 +1,257 @@ +import React from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import { Button, TextField } from '@material-ui/core'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import Dialog from '@material-ui/core/Dialog'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../../redux/store/RootState'; +import SelectBox, { SelectOption } from '../../common/SelectBox'; +import { getViewLayout, getLayouts } from '../../../redux/views/selectors'; +import { useState } from 'react'; +import { startLoadingAction, endLoadingAction, openErrorDialogAction } from '../../../redux/ui/actions'; +import { fetchLayoutsAction, backViewAction, selectedLayoutCommitAction, tempStoreViewAction } from '../../../redux/views/actions'; +import { getOpid } from '../../../redux/operations/selectors'; +import { Layout } from '../../../models/TelemetryView'; + +const useStyles = makeStyles( + createStyles({ + button: { + width: 120 + }, + paper: { + height: '80vh', + width: 500 + } +})); + +export interface OpenLayoutDialogProps { + classes: Record<'paper', string>; + keepMounted: boolean; + open: boolean; + onClose: () => void; +} + +const OpenLayoutDialog = (props: OpenLayoutDialogProps) => { + const { onClose, open } = props; + const classes = useStyles(); + const selector = useSelector((state: RootState) => state); + const dispatch = useDispatch(); + const [value, setValue] = useState(""); + const radioGroupRef = React.useRef(null); + const layouts = (getLayouts(selector)!=undefined)?getLayouts(selector):[]; + const layoutOptions: SelectOption[] = layouts.map(layout => ({ id: layout.id, name: layout.name })); + const templayout = getViewLayout(selector); + + const tempStoreView = () => { + dispatch(tempStoreViewAction(templayout)); + onClose(); + } + const backLayout = () => { + dispatch(backViewAction()); + onClose(); + } + + const opid = getOpid(selector); + const saveLayoutAsync = async (text: string) => { + var names: string[] = []; + var views: any[] = []; + for (var layout of layouts){ + names.push(layout.name); + views.push(layout.telemetryView); + } + if (names.includes(text)||text==""){ + setText(() => ""); + dispatch(openErrorDialogAction("invalid layout name")); + } + else if (views.includes(templayout)){ + setText(() => ""); + dispatch(openErrorDialogAction("invalid layout name")); + } + else { + const res = await fetch(`/api/operations/${opid}/lyt`,{ + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify({ + telemetryView: { + allIndexes: templayout.allIndexes, + blocks: templayout.blocks, + contents: templayout.contents + }, + id: layouts.length, + name: text + }) + }); + dispatch(endLoadingAction()); + setText(() => ""); + const res_lyts = await fetch(`/api/operations/${opid}/lyt`,{ + method: 'GET' + }); + const json_lyts = await res_lyts.json(); + const lyts = json_lyts.data as Layout[]; + dispatch(fetchLayoutsAction(lyts)); + } + onClose(); + } + + const deleteLayoutAsync = async () => { + const index = handleLayoutIndexChange(layoutIndex); + const name = layouts[index].name; + dispatch(startLoadingAction()); + const res = await fetch(`/api/operations/${opid}/lyt/${name}`,{ + method: 'DELETE' + }); + dispatch(endLoadingAction()); + setText(() => ""); + const res_lyts = await fetch(`/api/operations/${opid}/lyt`, { + method: 'GET' + }); + const json_lyts = await res_lyts.json(); + const lyts = json_lyts.data as Layout[]; + dispatch(fetchLayoutsAction(lyts)); + onClose(); + } + + const renameLayoutAsync = async (text: string) => { + var names: string[] = []; + var views: any[] = []; + for (var layout of layouts){ + names.push(layout.name); + views.push(layout.telemetryView); + } + if (names.includes(text)||text==""){ + setText(() => ""); + dispatch(openErrorDialogAction("invalid layout name")); + } + else{ + const index = handleLayoutIndexChange(layoutIndex); + dispatch(startLoadingAction()); + const res = await fetch(`/api/operations/${opid}/lyt/${index}/${text}`,{ + method: 'PUT' + }); + dispatch(endLoadingAction()); + setText(() => ""); + const res_lyts = await fetch(`/api/operations/${opid}/lyt`,{ + method: 'GET' + }); + const json_lyts = await res_lyts.json(); + const lyts = json_lyts.data as Layout[]; + dispatch(fetchLayoutsAction(lyts)); + } + onClose(); + } + + + const [text, setText] = useState(''); + const handleChange = (e: any) => { + setText(() => e.target.value) + } + + const handleEntering = () => { + if (radioGroupRef.current != null) { + radioGroupRef.current.focus(); + } + }; + + const handleCancel = () => { + onClose(); + setValue(""); + }; + + var [layoutIndex, setLayoutIndex] = useState(''); + + const handleLayoutIndexChange = (layoutIndex: string) => { + setLayoutIndex(layoutIndex); + const index: number = +layoutIndex; + return index; + } + + const handleOk = async () => { + const index = handleLayoutIndexChange(layoutIndex); + dispatch(selectedLayoutCommitAction(index)); + layoutIndex = ''; + onClose(); + }; + + return ( + + Temporary Save and Restore + +
+ + +
+
+ Save and Select + +
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + + +
+ ) +}; + +export default OpenLayoutDialog; diff --git a/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenPacketTabDialog.tsx b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenPacketTabDialog.tsx new file mode 100644 index 0000000..8d88181 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenPacketTabDialog.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import Dialog from '@material-ui/core/Dialog'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormGroup from '@material-ui/core/FormGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../../redux/store/RootState'; +import { SelectOption } from '../../common/SelectBox'; +import { TelemetryViewIndex } from '../../../models'; +import { getLatestTelemetries } from '../../../redux/telemetries/selectors'; +import { selectTelemetryAction } from '../../../redux/views/actions'; + +const useStyles = makeStyles( + createStyles({ + paper: { + height: '80vh', + width: 500 + } +})); + +export interface OpenPacketTabDialogProps { + blockNum: number, + classes: Record<'paper', string>; + keepMounted: boolean; + open: boolean; + tab: TelemetryViewIndex; + onClose: () => void; +} + +const OpenPacketTabDialog = (props: OpenPacketTabDialogProps) => { + const {tab, onClose, blockNum, open } = props; + const classes = useStyles(); + const selector = useSelector((state: RootState) => state); + const dispatch = useDispatch(); + + const formGroupRef = React.useRef(null); + + const latestTelemetries = getLatestTelemetries(selector)[tab.name]; + const telemetryOptions: SelectOption[] = latestTelemetries.map(latestTelemetry => ({id:latestTelemetry.telemetryInfo.name, name:latestTelemetry.telemetryInfo.name})); + + + interface CheckboxState { + [id: string] : boolean; + } + + let initCheckboxState : CheckboxState = {}; + telemetryOptions.forEach(telemetryOption => { + if (tab.selectedTelemetries.includes(telemetryOption.name)){ + initCheckboxState[telemetryOption.id] = true; + } + else{ + initCheckboxState[telemetryOption.id] = false; + } + }); + + const [checkboxState, setCheckboxState] = React.useState(initCheckboxState); + + const handleEntering = () => { + if(formGroupRef.current != null){ + formGroupRef.current.focus(); + } + }; + + const handleCancel = () => { + onClose(); + setCheckboxState(initCheckboxState); + }; + + const handleOk = () => { + let telemetryActive :string[] = []; + telemetryOptions.forEach(telemetryOption => { + if (checkboxState[telemetryOption.id] === true){ + telemetryActive.push(telemetryOption.name); + } + }); + dispatch(selectTelemetryAction(blockNum, telemetryActive)); + onClose(); + }; + + const handleChange = (event: React.ChangeEvent) => { + setCheckboxState({...checkboxState, [(event.target as HTMLInputElement).value]:event.target.checked}); + }; + + return ( + + + + {telemetryOptions.length > 0 && ( + telemetryOptions.map(telemetryOption => { + return } + label={telemetryOption.name} + /> + }) + )} + + + + + + + + ) +}; + +export default OpenPacketTabDialog; \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenViewDialog.tsx b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenViewDialog.tsx new file mode 100644 index 0000000..402bd74 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/OpenViewDialog.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import Dialog from '@material-ui/core/Dialog'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormGroup from '@material-ui/core/FormGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../../redux/store/RootState'; +import { getAllIndexes } from '../../../redux/views/selectors'; +import { openViewAction } from '../../../redux/views/actions'; +import SelectBox, { SelectOption } from '../../common/SelectBox'; +import { getLatestTelemetries } from '../../../redux/telemetries/selectors'; +import { selectTelemetryAction } from '../../../redux/views/actions'; + +const useStyles = makeStyles( + createStyles({ + paper: { + height: '80vh', + width: 500 + } +})); + +export interface OpenViewDialogProps { + blockNum: number, + classes: Record<'paper', string>; + keepMounted: boolean; + open: boolean; + onClose: () => void; +} + +const OpenViewDialog = (props: OpenViewDialogProps) => { + const { onClose, blockNum, open } = props; + const classes = useStyles(); + const selector = useSelector((state: RootState) => state); + const dispatch = useDispatch(); + + const formGroupRef = React.useRef(null); + const [type, setType] = React.useState(""); + const indexes = getAllIndexes(selector); + + interface CheckboxState { + [id: string] : boolean; + } + + let initCheckboxState : CheckboxState = {}; + indexes.forEach(element => initCheckboxState[element.id] = false); + const [checkboxState, setCheckboxState] = React.useState(initCheckboxState); + + const handleEntering = () => { + if(formGroupRef.current != null){ + formGroupRef.current.focus(); + } + }; + + const handleCancel = () => { + onClose(); + let makeCheckboxStateFalse = {...checkboxState}; + indexes.forEach(element => { + if (checkboxState[element.id] === true){ + makeCheckboxStateFalse[element.id] = false; + } + setCheckboxState(makeCheckboxStateFalse); + }) + }; + + const handleOk = () => { + let makeCheckboxStateFalse = {...checkboxState}; + indexes.forEach(element => { + if (checkboxState[element.id] === true){ + if (type === "packet" || "graph" && element.type === type){ + dispatch(openViewAction(blockNum, element.id, {})); + makeCheckboxStateFalse[element.id] = false; + } + if (type === "packet"){ + let telemetryShowed :string[] = []; + let tlms = getLatestTelemetries(selector)[element.name]; + tlms.forEach(tlm => { + telemetryShowed.push(tlm.telemetryInfo.name); + }) + dispatch(selectTelemetryAction(blockNum, telemetryShowed)); + } + } + }); + setCheckboxState(makeCheckboxStateFalse); + onClose(); + }; + + const handleChange = (event: React.ChangeEvent) => { + setCheckboxState({...checkboxState, [(event.target as HTMLInputElement).value]:event.target.checked}); + }; + + const typeOptions: SelectOption[] = ["packet", "character", "graph"].map(type => ({id: type, name: type})); + + return ( + + + + + + + {indexes.length > 0 && ( + indexes.map(index => { + if (index.type == type) { + return } + label={index.name} + /> + } + }) + )} + + + + + + + + ) +}; + +export default OpenViewDialog; \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/PacketTabPanel.tsx b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/PacketTabPanel.tsx new file mode 100644 index 0000000..3f0f41f --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/PacketTabPanel.tsx @@ -0,0 +1,194 @@ +import React from 'react'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; +import { useSelector, useDispatch } from 'react-redux'; +import { Telemetry, TelemetryViewIndex } from '../../../models'; +import { RootState } from '../../../redux/store/RootState'; +import { getLatestTelemetries } from '../../../redux/telemetries/selectors'; +import Toolbar from '@material-ui/core/Toolbar'; +import Radio from '@material-ui/core/Radio'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import FormControl from '@material-ui/core/FormControl'; +import FormLabel from '@material-ui/core/FormLabel'; +import Button from '@material-ui/core/Button'; +import { setTelemetryTypePacketAction } from '../../../redux/views/actions'; +import CenterFocusStrongIcon from '@material-ui/icons/CenterFocusStrong'; +import IconButtonInTabs from '../../common/IconButtonInTabs'; +import OpenPacketTabDialog from './OpenPacketTabDialog'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + padding: 10 + }, + item: { + [theme.breakpoints.down('sm')]: { + width: '100%' + }, + [theme.breakpoints.up('sm')]: { + width: '50%' + }, + [theme.breakpoints.up('md')]: { + width: '33.33%' + }, + }, + tlmul: { + paddingInlineStart: 0, + margin: 0 + }, + tlmli: { + fontSize: 'xx-small', + display: 'block', + "& span" : { + color: theme.palette.success.main + } + }, + dataTypeField: { + fontSize: "10pt", + textAlign:"center" + }, + dialogPaper: { + width: '80%', + maxHeight: 435, + } +})); + +export interface PacketTabPanelProps { + tab: TelemetryViewIndex, + blockNum: number +} + +const PacketTabPanel = (props: PacketTabPanelProps) => { + const { tab, blockNum } = props; + const selector = useSelector((state: RootState) => state); + const classes = useStyles(); + const dispatch = useDispatch(); + const tlms = getLatestTelemetries(selector)[tab.name]; + const selectedTelemetries = tab.selectedTelemetries; + const tlmClassList :[string[]] = [[tab.name]]; + + let tlmsDisplayed:Telemetry[] = []; + tlms.forEach(tlm => { + if (selectedTelemetries.indexOf(tlm.telemetryInfo.name)>=0){ + tlmsDisplayed.push(tlm); + } + }) + const num = tlmsDisplayed.length; + + const [dataType, setDataType] = React.useState(tab.dataType); + const [dialogOpen, setDialogOpen] = React.useState(false); + + const handleChange = (event: React.ChangeEvent) => { + setDataType((event.target as HTMLInputElement).value); + }; + + const handleOk = () => { + dispatch(setTelemetryTypePacketAction(blockNum, dataType)); + }; + + const handleDialogOpen = () => { + setDialogOpen(true); + } + + const handleDialogClose = () => { + setDialogOpen(false); + }; + + const showTlmData = (tlm: Telemetry) => { + const tlmClasses = tlm.telemetryInfo.name.split('.'); + const tlmClassesDisplayed: JSX.Element[] = []; + if (tlmClasses.length == 1){ + tlmClassesDisplayed.push( +
  • + {tlm.telemetryInfo.name} : {(tab.dataType != "Raw")? tlm.telemetryValue.value: tlm.telemetryValue.rawValue} +
  • + ) + tlmClassList.push([tlm.telemetryInfo.name]); + } + else { + const thisTlmClasses:string[] = []; + tlmClasses.forEach((tlmName, i) => { + let tlmClassesTmp = (i == 0)? tlmName :tlmClasses.slice(0,i+1).join("."); + if (i == tlmClasses.length-1) { + tlmClassesDisplayed.push( +
  • + {} + {tlmName} : {(tab.dataType != "Raw")? tlm.telemetryValue.value: tlm.telemetryValue.rawValue} +
  • + ) + } + else if (tlmClassList[tlmClassList.length-1].indexOf(tlmClassesTmp) == -1) { + tlmClassesDisplayed.push( +
  • + {} + {tlmName} +
  • + ) + } + thisTlmClasses.push(tlmClassesTmp); + }) + tlmClassList.push(thisTlmClasses); + } + return ( + <> + {(tlmClassesDisplayed.map((tlmClass) => tlmClass))} + + ) + }; + + return ( +
    + + + Data Type + + + } label="Default" /> + } label="Raw" /> + + + + + + + + + + + + {tlmsDisplayed.filter((tlm,i) => i < num/3) + .map(tlm => ( + showTlmData(tlm) + ))} + + + {tlmsDisplayed.filter((tlm,i) => i >= num/3 && i < 2*num/3) + .map(tlm => ( + showTlmData(tlm) + ))} + + + {tlmsDisplayed.filter((tlm,i) => i >= 2*num/3 && i < num) + .map(tlm => ( + showTlmData(tlm) + ))} + + +
    + ) +}; + +export default PacketTabPanel; diff --git a/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/ViewDisplayBlock.tsx b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/ViewDisplayBlock.tsx new file mode 100644 index 0000000..467c245 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/ViewDisplayBlock.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import AppBar from '@material-ui/core/AppBar'; +import Tabs from '@material-ui/core/Tabs'; +import Tab from '@material-ui/core/Tab'; +import ViewTabPanel from './ViewTabPanel'; +import AddIcon from '@material-ui/icons/Add'; +import CloseIcon from '@material-ui/icons/Close'; +import IconButtonInTabs from '../../common/IconButtonInTabs'; +import { ViewBlockInfo } from '../../../models'; +import { activateViewAction, closeViewAction } from '../../../redux/views/actions'; +import OpenViewDialog from './OpenViewDialog'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + [theme.breakpoints.down('lg')]: { + width: '100%' + }, + [theme.breakpoints.up('lg')]: { + width: 'calc(50% - 1rem)' + }, + "& .MuiTab-root": { + minWidth: 100 + }, + minHeight: 'calc(50vh - 4.5rem)', + margin: 2, + borderStyle: "solid", + borderColor: theme.palette.primary.light, + borderWidth: "0px 1px 1px 1px" + }, + tab: { + width: 120, + minHeight: "auto", + textAlign: "left", + padding: "0", + "& span": { + whiteSpace: "nowrap", + textOverflow: "ellipsis", + overflow: "hidden", + display: "inline-block", + flexGrow: 0 + }, + "& .MuiTab-wrapper > *:first-child": { + marginBottom: 0, + padding: 0, + marginRight: 15, + marginLeft: 5, + width: 20, + height: 20 + } + }, + dialogPaper: { + width: '80%', + maxHeight: 435, + }, +})); + +const a11yProps = (index: any) => { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; +} + +export interface ViewDisplayBlockProps { + blockInfo: ViewBlockInfo, + blockNum: number +} + +const ViewDisplayBlock = (props: ViewDisplayBlockProps) => { + const { blockInfo, blockNum } = props; + const classes = useStyles(); + const dispatch = useDispatch(); + const value = blockInfo.activeTab; + + const [dialogOpen, setDialogOpen] = React.useState(false); + + const handleDialogOpen = () => { + setDialogOpen(true); + } + + const handleDialogClose = () => { + setDialogOpen(false); + }; + + const handleValueChange = (event: React.ChangeEvent<{}>, newValue: number) => { + dispatch(activateViewAction(blockNum, newValue)) + }; + + const closeView = (tab: number) => { + dispatch(closeViewAction(blockNum, tab)) + } + + interface CloseIconInTabProps { + onClick: (event: React.MouseEvent) => void + } + + const CloseIconInTab = (props: CloseIconInTabProps) => { + return ( + + + + ); + }; + + return ( +
    + + + {blockInfo.tabs.map((tab,i) => ( + closeView(i)}/>} + /> + ))} + + + + + + {blockInfo.tabs.length > 0 ? ( + blockInfo.tabs.map((tab,i) => ( + + )) + ) : ( +
    + )} + +
    + ); +}; + +export default ViewDisplayBlock; diff --git a/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/ViewTabPanel.tsx b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/ViewTabPanel.tsx new file mode 100644 index 0000000..11794f5 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/components/telemetry/view_display/ViewTabPanel.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { TelemetryViewIndex } from '../../../models'; +import PacketTabPanel from './PacketTabPanel'; +import GraphTabPanel from './GraphTabPanel'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../../redux/store/RootState'; +import { getViewContents } from '../../../redux/views/selectors'; + + +export interface ViewTabPanelProps { + tab: TelemetryViewIndex, + index: any; + value: any; + blockNum: number +} + +const ViewTabPanel = (props: ViewTabPanelProps) => { + const { tab, value, index, blockNum } = props; + const selector = useSelector((state: RootState) => state); + const contents = getViewContents(selector); + + if (value !== index) { + return <> + }; + + return ( +
    + {(() => { + switch (tab.type) { + case "packet": { + return ; + } + + // case "character": { + // const content = contents[tab.id]; + // return ; + // } + + case "graph": { + const content = contents[tab.id]; + return ; + } + + default: + break; + } + })()} +
    + ); +} + +export default ViewTabPanel; diff --git a/aspnetapp/WINGS/ClientApp/src/constants/index.ts b/aspnetapp/WINGS/ClientApp/src/constants/index.ts new file mode 100644 index 0000000..c098b78 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/constants/index.ts @@ -0,0 +1,3 @@ +export const UNPLANNED_ID = "_unplanned"; +export const TARGET_ALL = "ALL"; +export const COMPONENT_ALL = "ALL"; diff --git a/aspnetapp/WINGS/ClientApp/src/index.tsx b/aspnetapp/WINGS/ClientApp/src/index.tsx new file mode 100644 index 0000000..b34c730 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { ConnectedRouter } from 'connected-react-router' ; +import * as History from 'history'; +import { MuiThemeProvider } from '@material-ui/core'; +import { theme } from './assets/theme'; +import App from './App'; +import createStore from './redux/store/store'; + +const history = History.createBrowserHistory(); +export const store = createStore(history); + +ReactDOM.render( + + + + + + + , + document.getElementById('root') +); diff --git a/aspnetapp/WINGS/ClientApp/src/models/Command.ts b/aspnetapp/WINGS/ClientApp/src/models/Command.ts new file mode 100644 index 0000000..a1574c0 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/Command.ts @@ -0,0 +1,35 @@ +export type CommandParam = { + name: string | null, + type: string, + value: string | null, + unit: string | null, + description: string | null +} + +export type Command = { + component: string, + execType: string, + execTime: number, + name: string, + code: string, + target: string, + params: CommandParam[], + isDanger: boolean, + isViaMobc: boolean, + isRestricted: boolean, + description: string +} + +export type CommandState = { + list: Command[], + targets: string[], + components: string[], + logs: CommandFileLineLogs[] +} + +export type CommandFileLineLogs = { + time: string, + commander: string, + content: string, + status: string +} diff --git a/aspnetapp/WINGS/ClientApp/src/models/CommandPlan.ts b/aspnetapp/WINGS/ClientApp/src/models/CommandPlan.ts new file mode 100644 index 0000000..3f27c6e --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/CommandPlan.ts @@ -0,0 +1,43 @@ +import { Command } from './Command'; +import { FileIndex } from './FileIndex'; + +export type CommandPlanIndex = FileIndex & { + fileId: string, + cmdFileInfoIndex: string +} + +export type Request = { + type: string, + method: string | null, + body: string | Command | any, + inlineComment: string | null, + stopFlag: boolean, + syntaxError: boolean, + errorMessage: string | null +} + +export type RequestStatus = { + success: boolean, + error: boolean +} + +export type CommandPlanLine = { + status: RequestStatus, + request: Request +} + +export type CommandPlanState = { + allIndexes: CommandPlanIndex[], + openedIds: string[], + activeId: string, + selectedRow: number, + contents : { + [id: string] : CommandPlanLine[] + }, + selectedCommand: { + component: string, + target: string, + command: Command + }, + inExecution: boolean +} diff --git a/aspnetapp/WINGS/ClientApp/src/models/Component.ts b/aspnetapp/WINGS/ClientApp/src/models/Component.ts new file mode 100644 index 0000000..cc509e8 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/Component.ts @@ -0,0 +1,7 @@ +export type Component = { + id: string, + name: string, + tcPacketKey: string, + tmPacketKey: string, + localDirPath: string +} \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/models/ErrorDialogState.ts b/aspnetapp/WINGS/ClientApp/src/models/ErrorDialogState.ts new file mode 100644 index 0000000..7fa2fc5 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/ErrorDialogState.ts @@ -0,0 +1,4 @@ +export type ErrorDialogState = { + open: boolean, + message: string +} diff --git a/aspnetapp/WINGS/ClientApp/src/models/FileIndex.ts b/aspnetapp/WINGS/ClientApp/src/models/FileIndex.ts new file mode 100644 index 0000000..fe8fe0f --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/FileIndex.ts @@ -0,0 +1,5 @@ +export type FileIndex = { + id: string, + name: string, + filePath: string +}; diff --git a/aspnetapp/WINGS/ClientApp/src/models/LocationState.ts b/aspnetapp/WINGS/ClientApp/src/models/LocationState.ts new file mode 100644 index 0000000..22a668e --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/LocationState.ts @@ -0,0 +1,3 @@ +export type LocationState = { + data: T +}; diff --git a/aspnetapp/WINGS/ClientApp/src/models/Operation.ts b/aspnetapp/WINGS/ClientApp/src/models/Operation.ts new file mode 100644 index 0000000..7bcfdf6 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/Operation.ts @@ -0,0 +1,22 @@ +import { Component } from './Component'; + +export type Operation = { + id: string, + pathNumber: string, + comment: string, + isRunning: boolean, + isTmtcConncted: boolean, + fileLocation: string, + tmtcTarget: string | null, + operatorId: string | null, + operator: { + id: string, + userName: string, + role: string + }, + componentId: string, + component: Component, + createdAt: string, + satelliteId: string | null, + planId: string | null +} diff --git a/aspnetapp/WINGS/ClientApp/src/models/PaginationMeta.ts b/aspnetapp/WINGS/ClientApp/src/models/PaginationMeta.ts new file mode 100644 index 0000000..6d740b4 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/PaginationMeta.ts @@ -0,0 +1,12 @@ +export type PaginationMeta = { + page: number, + size: number, + pageCount: number, + links: { + self: string, + first: string, + previous: string, + next: string, + last: string + } +}; diff --git a/aspnetapp/WINGS/ClientApp/src/models/Telemetry.ts b/aspnetapp/WINGS/ClientApp/src/models/Telemetry.ts new file mode 100644 index 0000000..badf382 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/Telemetry.ts @@ -0,0 +1,59 @@ +export type Telemetry = { + telemetryInfo: TelemetryInfo, + telemetryValue: TelemetryValue +} + +export type TelemetryHistory = { + telemetryInfo: TelemetryInfo, + telemetryValues: TelemetryValue[] +} + +export type TelemetryInfo = { + name: string, + type: string, + octetPos: number, + bitPos: number, + bitLen: number, + convType: string, + poly: number[], + status: { + [value: string]: string + }, + description: string +} + +export type TelemetryValue = { + time: string, + value: string, + rawValue: string, + TI: string +} + +export type TelemetryPacket = { + packetInfo: { + id: string, + name: string, + isRealtimeData: boolean, + isRestricted: boolean + }, + telemetries: Telemetry[] +} + +export type TelemetryPacketHistory = { + packetInfo: { + id: string, + name: string, + isRealtimeData: boolean, + isRestricted: boolean + }, + telemetryHistories: TelemetryHistory[] +} + +export type TelemetryState = { + latest: { + [packetName: string]: Telemetry[] + }, + history: { + [packetName: string]: TelemetryHistory[] + } +} diff --git a/aspnetapp/WINGS/ClientApp/src/models/TelemetryView.ts b/aspnetapp/WINGS/ClientApp/src/models/TelemetryView.ts new file mode 100644 index 0000000..472106b --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/TelemetryView.ts @@ -0,0 +1,35 @@ +import { FileIndex } from "./FileIndex" + +export type TelemetryViewIndex = FileIndex & { + type: "packet" | "character" | "graph" | "", + selectedTelemetries: string[], + dataType: string, + dataLength: string, + ylabelMin: string, + ylabelMax: string +} + +export type ViewBlockInfo = { + tabs: TelemetryViewIndex[], + activeTab: number +} + +export type TelemetryView = { + allIndexes: TelemetryViewIndex[], + blocks: ViewBlockInfo[], + contents: { + [id: string]: any + }, +} + +export type Layout = { + telemetryView: TelemetryView, + id: number, + name: string +} + +export type TelemetryViewState = { + currentView: TelemetryView, + tempStoredView: TelemetryView, + layoutList: Layout[] +} diff --git a/aspnetapp/WINGS/ClientApp/src/models/UIState.ts b/aspnetapp/WINGS/ClientApp/src/models/UIState.ts new file mode 100644 index 0000000..0ce8894 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/UIState.ts @@ -0,0 +1,6 @@ +import { ErrorDialogState } from './ErrorDialogState'; + +export type UIState = { + isLoading: boolean, + errorDialog: ErrorDialogState +}; diff --git a/aspnetapp/WINGS/ClientApp/src/models/index.ts b/aspnetapp/WINGS/ClientApp/src/models/index.ts new file mode 100644 index 0000000..26aa3e6 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/models/index.ts @@ -0,0 +1,11 @@ +export * from './Component'; +export * from './Operation'; +export * from './Command'; +export * from './CommandPlan'; +export * from './Telemetry'; +export * from './TelemetryView'; +export * from './PaginationMeta'; +export * from './LocationState'; +export * from './ErrorDialogState'; +export * from './UIState'; +export * from './FileIndex'; diff --git a/aspnetapp/WINGS/ClientApp/src/react-app-env.d.ts b/aspnetapp/WINGS/ClientApp/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/aspnetapp/WINGS/ClientApp/src/redux/commands/actions.ts b/aspnetapp/WINGS/ClientApp/src/redux/commands/actions.ts new file mode 100644 index 0000000..4543b45 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/commands/actions.ts @@ -0,0 +1,18 @@ +import { Command, CommandFileLineLogs } from "../../models"; + +export const FETCH_COMMANDS = 'FETCH_COMMANDS' as const; +export const UPDATE_COMMAND_LOG = 'UPDATE_COMMAND_LOG' as const; + +export const fetchCommandsAction = (commands: Command[]) => { + return { + type: FETCH_COMMANDS, + payload: commands + }; +}; + +export const updateCommandLogAction = (commandLog: CommandFileLineLogs[]) => { + return { + type: UPDATE_COMMAND_LOG, + payload: commandLog + }; +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/commands/reducers.ts b/aspnetapp/WINGS/ClientApp/src/redux/commands/reducers.ts new file mode 100644 index 0000000..6b24407 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/commands/reducers.ts @@ -0,0 +1,37 @@ +import * as Actions from './actions'; +import * as OperationActions from '../operations/actions'; +import initialState from '../store/initialState'; + +type Actions = + | ReturnType + | ReturnType + | ReturnType +; + +export const CommandsReducer = (state = initialState.cmds, action: Actions) => { + switch (action.type) { + case Actions.FETCH_COMMANDS: + const commands = action.payload; + const targets = Array.from(new Set(commands.map(command => command.target))); + const components = Array.from(new Set(commands.map(command => command.component))); + return { + list: commands, + targets: [...state.targets, ...targets], + components: [...state.components, ...components], + logs: [] + } + + case Actions.UPDATE_COMMAND_LOG: + const cmdFileLineLogs = action.payload; + return { + ...state, + logs: cmdFileLineLogs + }; + + case OperationActions.LEAVE_OPERATION: + return initialState.cmds; + + default: + return state; + } +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/commands/selectors.ts b/aspnetapp/WINGS/ClientApp/src/redux/commands/selectors.ts new file mode 100644 index 0000000..078afbd --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/commands/selectors.ts @@ -0,0 +1,24 @@ +import { createSelector } from 'reselect'; +import { RootState } from '../store/RootState'; + +const commandsSelector = (state: RootState) => state.cmds; + +export const getCommands = createSelector( + [commandsSelector], + state => state.list +); + +export const getTargets = createSelector( + [commandsSelector], + state => state.targets +); + +export const getComponents = createSelector( + [commandsSelector], + state => state.components +); + +export const getCommandLogs = createSelector( + [commandsSelector], + state => state.logs +); \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/redux/operations/actions.ts b/aspnetapp/WINGS/ClientApp/src/redux/operations/actions.ts new file mode 100644 index 0000000..a0c30b4 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/operations/actions.ts @@ -0,0 +1,17 @@ +import { Operation } from "../../models"; + +export const JOIN_OPERATION = 'JOIN_OPERATION' as const; +export const LEAVE_OPERATION = 'LEAVE_OPERATION' as const; + +export const joinOperationAction = (operation: Operation) => { + return { + type: JOIN_OPERATION, + payload: operation + }; +}; + +export const leaveOperationAction = () => { + return { + type: LEAVE_OPERATION + }; +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/operations/operations.ts b/aspnetapp/WINGS/ClientApp/src/redux/operations/operations.ts new file mode 100644 index 0000000..9cc5f46 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/operations/operations.ts @@ -0,0 +1,74 @@ +import { Dispatch } from "redux"; +import { Operation, Command, CommandPlanIndex, TelemetryPacket, TelemetryPacketHistory, Layout, TelemetryViewIndex, CommandFileLineLogs } from "../../models"; +import { joinOperationAction, leaveOperationAction } from './actions'; +import { fetchCommandsAction } from '../commands/actions'; +import { fetchPlanIndexesAction } from '../plans/actions'; +import { fetchViewIndexesAction } from '../views/actions'; +import { fetchLayoutsAction } from '../views/actions'; +import { updateTelemetryHistoriesAction } from '../telemetries/actions'; +import { updateCommandLogAction } from "../commands/actions"; + +export const joinOperation = (operation: Operation) => { + return async (dispatch: Dispatch) => { + const opid = operation.id; + + dispatch(leaveOperationAction()); + dispatch(joinOperationAction(operation)); + + const resCmds = await fetch(`/api/operations/${opid}/cmd`, { + method: 'GET' + }); + if (resCmds.status == 200) { + const jsonCmds = await resCmds.json(); + const cmds = jsonCmds.data as Command[]; + dispatch(fetchCommandsAction(cmds)); + } + + const resPlans = await fetch(`/api/operations/${opid}/cmd_plans`, { + method: 'GET' + }); + if (resPlans.status == 200) { + const jsonPlans = await resPlans.json(); + const indexesNum = jsonPlans.data; + const planIndexes: CommandPlanIndex[] = indexesNum.length > 0 ? indexesNum.map((idx: any, index: number) => ({ ...idx, id: String(index) })) : []; + dispatch(fetchPlanIndexesAction(planIndexes)); + } + + const resCmdLog = await fetch(`/api/operations/${opid}/cmd_fileline/log`, { + method: 'GET' + }); + const jsonCmdLog = await resCmdLog.json(); + const dataCmdLog: CommandFileLineLogs[] = jsonCmdLog.data; + dispatch(updateCommandLogAction(dataCmdLog)); + + const resTlms = await fetch(`/api/operations/${opid}/tlm`, { + method: 'GET' + }); + if (resTlms.status == 200) { + const jsonTlms = await resTlms.json(); + const tlmPackets = jsonTlms.data as TelemetryPacket[]; + const packetIndexes: TelemetryViewIndex[] = tlmPackets.map(packet => ({ id: packet.packetInfo.name + "_packet", name: packet.packetInfo.name, filePath: "", type: "packet", selectedTelemetries: [], dataType: "Default", dataLength: "", ylabelMin: "", ylabelMax: "" })); + const graphIndexes: TelemetryViewIndex[] = tlmPackets.map(packet => ({ id: packet.packetInfo.name + "_graph", name: packet.packetInfo.name, filePath: "", type: "graph", selectedTelemetries: [], dataType: "Default", dataLength: "", ylabelMin: "", ylabelMax: "" })); + const viewIndexes = [...packetIndexes, ...graphIndexes]; + dispatch(fetchViewIndexesAction(viewIndexes)); + + const dataTlmHistory: TelemetryPacketHistory[] = []; + tlmPackets.forEach(tlmPacket => { + const tlmPacketHistoryTmp: TelemetryPacketHistory = { + packetInfo: tlmPacket.packetInfo, telemetryHistories: tlmPacket.telemetries.map(tlm => ({ telemetryInfo: tlm.telemetryInfo, telemetryValues: [tlm.telemetryValue] })) + }; + dataTlmHistory.push(tlmPacketHistoryTmp); + }); + dispatch(updateTelemetryHistoriesAction(dataTlmHistory)); + } + + const resLyts = await fetch(`/api/operations/${opid}/lyt`, { + method: 'GET' + }); + if (resLyts.status == 200) { + const jsonLyts = await resLyts.json(); + const lyts = jsonLyts.data as Layout[]; + dispatch(fetchLayoutsAction(lyts)); + } + } +} diff --git a/aspnetapp/WINGS/ClientApp/src/redux/operations/reducers.ts b/aspnetapp/WINGS/ClientApp/src/redux/operations/reducers.ts new file mode 100644 index 0000000..90c6118 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/operations/reducers.ts @@ -0,0 +1,20 @@ +import * as Actions from './actions'; +import initialState from '../store/initialState'; + +type Actions = + | ReturnType + | ReturnType +; + +export const OperationsReducer = (state = initialState.operation, action: Actions) => { + switch (action.type) { + case Actions.JOIN_OPERATION: + return action.payload; + + case Actions.LEAVE_OPERATION: + return initialState.operation; + + default: + return state; + } +} \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/redux/operations/selectors.ts b/aspnetapp/WINGS/ClientApp/src/redux/operations/selectors.ts new file mode 100644 index 0000000..d351a3d --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/operations/selectors.ts @@ -0,0 +1,14 @@ +import { createSelector } from 'reselect'; +import { RootState } from '../store/RootState'; + +const operationsSelector = (state: RootState) => state.operation; + +export const getOpid = createSelector( + [operationsSelector], + state => state.id +); + +export const getCurrentOperation = createSelector( + [operationsSelector], + state => state +); diff --git a/aspnetapp/WINGS/ClientApp/src/redux/plans/actions.ts b/aspnetapp/WINGS/ClientApp/src/redux/plans/actions.ts new file mode 100644 index 0000000..632b1f0 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/plans/actions.ts @@ -0,0 +1,106 @@ +import { Command, FileIndex, Request } from "../../models"; + +export const FETCH_PLAN_INDEXES = 'FETCH_PLAN_INDEXES' as const; +export const OPEN_PLAN = 'OPEN_PLAN' as const; +export const CLOSE_PLAN = 'CLOSE_PLAN' as const; +export const ACTIVATE_PLAN = 'ACTIVATE_PLAN' as const; +export const SELECT_PLAN_ROW = 'SELECT_PLAN_ROW' as const; +export const EXEC_REQUEST_SUCCESS = 'EXEC_REQUEST_SUCCESS' as const; +export const EXEC_REQUEST_ERROR = 'EXEC_REQUEST_ERROR' as const; +export const EXEC_REQUESTS_START = 'EXEC_REQUESTS_START' as const; +export const EXEC_REQUESTS_END = 'EXEC_REQUESTS_END' as const; +export const SELECTED_COMPONENT_EDIT = 'SELECTED_COMPONENT_EDIT' as const; +export const SELECTED_TARGET_EDIT = 'SELECTED_TARGET_EDIT' as const; +export const SELECTED_COMMAND_EDIT = 'SELECTED_COMMAND_EDIT' as const; +export const SELECTED_COMMAND_COMMIT = 'SELECTED_COMMAND_COMMIT' as const; + +export const fetchPlanIndexesAction = (indexes: FileIndex[]) => { + return { + type: FETCH_PLAN_INDEXES, + payload: indexes + }; +}; + +export const openPlanAction = (id: string, requests: Request[]) => { + return { + type: OPEN_PLAN, + payload: { + id: id, + requests: requests + } + }; +}; + +export const closePlanAction = (id: string) => { + return { + type: CLOSE_PLAN, + payload: id + }; +} + +export const activatePlanAction = (id: string) => { + return { + type: ACTIVATE_PLAN, + payload: id + }; +}; + +export const selectedPlanRowAction = (row: number) => { + return { + type: SELECT_PLAN_ROW, + payload: row + } +}; + +export const execRequestSuccessAction = (row: number) => { + return { + type: EXEC_REQUEST_SUCCESS, + payload: row + } +}; + +export const execRequestErrorAction = (row: number) => { + return { + type: EXEC_REQUEST_ERROR, + payload: row + } +}; + +export const execRequestsStartAction = () => { + return { + type: EXEC_REQUESTS_START + } +} + +export const execRequestsEndAction = () => { + return { + type: EXEC_REQUESTS_END + } +} + +export const selectedComponentEditAction = (component: string) => { + return { + type: SELECTED_COMPONENT_EDIT, + payload: component + } +}; + +export const selectedTargetEditAction = (target: string) => { + return { + type: SELECTED_TARGET_EDIT, + payload: target + } +}; + +export const selectedCommandEditAction = (command: Command) => { + return { + type: SELECTED_COMMAND_EDIT, + payload: command + } +}; + +export const selectedCommandCommitAction = () => { + return { + type: SELECTED_COMMAND_COMMIT + } +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/plans/operations.ts b/aspnetapp/WINGS/ClientApp/src/redux/plans/operations.ts new file mode 100644 index 0000000..87314a5 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/plans/operations.ts @@ -0,0 +1,59 @@ +import { Dispatch } from "redux"; +import { RootState } from "../store/RootState"; +import { Request, CommandPlanLine } from '../../models'; +import * as Actions from './actions'; +import * as UiActions from '../ui/actions'; + +export const openPlan = (id: string) => { + return async (dispatch: Dispatch, getState: () => RootState) => { + const opid = getState().operation.id; + const indexTemp = getState().plans.allIndexes.find(a => a.id === id); + const cmdFileInfoIndex = (indexTemp != null)?indexTemp.cmdFileInfoIndex:""; + const fileId = (indexTemp != null) ?indexTemp.fileId:""; + const res = await fetch(`/api/operations/${opid}/cmd_plans/${cmdFileInfoIndex}/${fileId}`, { + method: 'GET' + }); + const json = await res.json(); + if (res.status === 200) { + const data = json.data.content as Request[]; + dispatch(Actions.openPlanAction(id, data)); + } else { + const message = `Status Code: ${res.status}\n${json.message ? json.message: "unknown error"}`; + dispatch(UiActions.openErrorDialogAction(message)); + } + } +}; + +export const postCommand = (row: number, req: Request, ret: boolean[]) => { + return async (dispatch: Dispatch, getState: () => RootState) => { + const opid = getState().operation.id; + const res = await fetch(`/api/operations/${opid}/cmd`,{ + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify({ command : req.body }) + }); + const json = await res.json(); + if (res.status === 200 && json.ack) { + await dispatch(Actions.execRequestSuccessAction(row)); + ret[0] = true; + } else { + await dispatch(Actions.execRequestErrorAction(row)); + ret[0] = false; + } + } +}; + +export const postCommandFileLineLog = (content: CommandPlanLine) => { + return async (dispatch: Dispatch, getState: () => RootState) => { + const opid = getState().operation.id; + const res = await fetch(`/api/operations/${opid}/cmd_fileline_log`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ command_file_line_log: content }) + }); + const json = await res.json(); + } +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/plans/reducers.ts b/aspnetapp/WINGS/ClientApp/src/redux/plans/reducers.ts new file mode 100644 index 0000000..0aa1d4d --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/plans/reducers.ts @@ -0,0 +1,228 @@ +import * as Actions from './actions'; +import * as OperationActions from '../operations/actions'; +import initialState from '../store/initialState'; +import { CommandPlanLine } from '../../models'; +import { UNPLANNED_ID } from '../../constants'; + +type Actions = + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType +; + +export const PlansReducer = (state = initialState.plans, action: Actions) => { + switch (action.type) { + case Actions.FETCH_PLAN_INDEXES: { + return { + ...state, + allIndexes: [...state.allIndexes, ...action.payload] + }; + } + + case Actions.OPEN_PLAN: { + const id = action.payload.id; + const openedIds = state.openedIds; + let newOpenedIds = openedIds; + if (!openedIds.includes(id)) { + newOpenedIds = [...openedIds, id]; + } + const content = action.payload.requests.map( + request => ({ + status: { + success: false, + error: false + }, + request: request + } as CommandPlanLine) + ) + return { + ...state, + openedIds: newOpenedIds, + activeId: id, + selectedRow: -1, + contents: { + ...state.contents, + [id]: content + } + } + } + + case Actions.CLOSE_PLAN: { + const closeId = action.payload; + const newOpenedIds = state.openedIds.filter(id => id !== closeId); + const newActiveId = state.activeId === closeId ? initialState.plans.activeId : state.activeId; + const {[closeId]: closeContent, ...newContents} = state.contents; + return { + ...state, + openedIds: newOpenedIds, + activeId: newActiveId, + contents: newContents + }; + } + + case Actions.ACTIVATE_PLAN: { + const id = action.payload; + const newActiveId = state.openedIds.includes(id) ? id : state.activeId; + return { + ...state, + activeId: newActiveId, + selectedRow: -1 + } + } + + case Actions.SELECT_PLAN_ROW: { + return { + ...state, + selectedRow: action.payload + }; + } + + case Actions.EXEC_REQUEST_SUCCESS: { + const row = action.payload; + const activeId = state.activeId; + return { + ...state, + contents: { + ...state.contents, + [activeId]: [ + ...state.contents[activeId].slice(0,row), + { + ...state.contents[activeId][row], + status: { + success: true, + error: false + } + }, + ...state.contents[activeId].slice(row+1) + ] + } + } + } + + case Actions.EXEC_REQUEST_ERROR: { + const row = action.payload; + const activeId = state.activeId; + return { + ...state, + contents: { + ...state.contents, + [activeId]: [ + ...state.contents[activeId].slice(0,row), + { + ...state.contents[activeId][row], + status: { + success: false, + error: true + } + }, + ...state.contents[activeId].slice(row+1) + ] + } + } + } + + case Actions.EXEC_REQUESTS_START: { + return { + ...state, + inExecution: true + } + } + + case Actions.EXEC_REQUESTS_END: { + return { + ...state, + inExecution: false + } + } + + case Actions.SELECTED_COMPONENT_EDIT: { + return { + ...state, + selectedCommand: { + component: action.payload, + target: state.selectedCommand.target, + command: initialState.plans.selectedCommand.command + } + }; + } + + case Actions.SELECTED_TARGET_EDIT: { + return { + ...state, + selectedCommand: { + component: state.selectedCommand.component, + target: action.payload, + command: initialState.plans.selectedCommand.command + } + }; + } + + case Actions.SELECTED_COMMAND_EDIT: { + return { + ...state, + selectedCommand: { + ...state.selectedCommand, + command: action.payload + } + }; + } + + case Actions.SELECTED_COMMAND_COMMIT: { + const line: CommandPlanLine = { + status: { + success: false, + error: false + }, + request: { + type: "command", + method: null, + body: state.selectedCommand.command, + inlineComment: null, + stopFlag: true, + syntaxError: false, + errorMessage: null + } + } + + let newContent: CommandPlanLine[]; + let newSelctedRow = state.selectedRow; + if (state.activeId === UNPLANNED_ID && state.selectedRow !== -1) { + newContent = [ + ...state.contents[UNPLANNED_ID].slice(0,state.selectedRow+1), + line, + ...state.contents[UNPLANNED_ID].slice(state.selectedRow+1) + ]; + newSelctedRow += 1; + } else { + newContent = [...state.contents[UNPLANNED_ID], line]; + } + + return { + ...state, + selectedRow: newSelctedRow, + contents: { + ...state.contents, + [UNPLANNED_ID]: newContent + } + } + } + + case OperationActions.LEAVE_OPERATION: { + return initialState.plans; + } + + default: + return state; + } +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/plans/selectors.ts b/aspnetapp/WINGS/ClientApp/src/redux/plans/selectors.ts new file mode 100644 index 0000000..7a52c32 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/plans/selectors.ts @@ -0,0 +1,46 @@ +import { createSelector } from 'reselect'; +import { FileIndex } from '../../models'; +import { RootState } from '../store/RootState'; + +const plansSelector = (state: RootState) => state.plans; + +export const getAllIndexes = createSelector( + [plansSelector], + state => state.allIndexes +); + +export const getActivePlanId = createSelector( + [plansSelector], + state => state.activeId +); + +export const getOpenedPlanIds = createSelector( + [plansSelector], + state => state.openedIds +); + +export const getOpenedPlanIndexes = createSelector( + [plansSelector], + state => state.openedIds.map(id => state.allIndexes.find(index => index.id === id)) as FileIndex[] +); + +export const getPlanContents = createSelector( + [plansSelector], + state => state.contents +); + +export const getSelectedRow = createSelector( + [plansSelector], + state => state.selectedRow +); + +export const getSelectedCommand = createSelector( + [plansSelector], + state => state.selectedCommand +); + +export const getInExecution = createSelector( + [plansSelector], + state => state.inExecution +); + diff --git a/aspnetapp/WINGS/ClientApp/src/redux/store/RootState.ts b/aspnetapp/WINGS/ClientApp/src/redux/store/RootState.ts new file mode 100644 index 0000000..be0c6d2 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/store/RootState.ts @@ -0,0 +1,10 @@ +import { Operation, CommandState, TelemetryState, CommandPlanState, UIState, TelemetryViewState, Layout } from "../../models"; + +export type RootState = { + ui: UIState, + operation: Operation, + cmds: CommandState, + tlms: TelemetryState, + plans: CommandPlanState, + views: TelemetryViewState +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/store/initialState.ts b/aspnetapp/WINGS/ClientApp/src/redux/store/initialState.ts new file mode 100644 index 0000000..088ec07 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/store/initialState.ts @@ -0,0 +1,102 @@ +import { RootState } from './RootState'; +import { UNPLANNED_ID, TARGET_ALL, COMPONENT_ALL } from '../../constants'; + +const initialState: RootState = { + ui: { + isLoading: false, + errorDialog: { + open: false, + message: "" + } + }, + operation: { + id: "", + pathNumber: "", + comment: "", + isRunning: false, + isTmtcConncted: false, + fileLocation: "", + tmtcTarget: null, + operatorId: null, + operator: { + id: "", + userName: "", + role: "" + }, + componentId: "", + component: { + id: "", + name: "", + tcPacketKey: "", + tmPacketKey: "", + localDirPath: "" + }, + createdAt: "", + satelliteId: null, + planId: null + }, + cmds: { + list: [], + targets: [TARGET_ALL], + components: [COMPONENT_ALL], + logs: [] + }, + tlms: { + latest: {}, + history: {} + }, + plans: { + allIndexes: [{ + id: UNPLANNED_ID, + fileId: "", + name: "Unplanned Commands", + filePath: "", + cmdFileInfoIndex: "" + }], + openedIds: [UNPLANNED_ID], + activeId: UNPLANNED_ID, + selectedRow: -1, + contents: { + [UNPLANNED_ID]: [] + }, + selectedCommand: { + component: COMPONENT_ALL, + target: TARGET_ALL, + command: { + component: "", + execType: "RT", + execTime: 0, + name: "", + code: "", + target: "", + params: [], + isDanger: false, + isViaMobc: false, + isRestricted: false, + description: "" + } + }, + inExecution: false + }, + views: { + currentView:{ + allIndexes: [], + blocks: Array(4).fill({ + tabs: [], + activeTab: 0 + }), + contents: {} + }, + tempStoredView:{ + allIndexes: [], + blocks: Array(4).fill({ + tabs: [], + activeTab: 0 + }), + contents: {} + }, + layoutList: [] + } +}; + +export default initialState; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/store/store.ts b/aspnetapp/WINGS/ClientApp/src/redux/store/store.ts new file mode 100644 index 0000000..368ae30 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/store/store.ts @@ -0,0 +1,41 @@ +import { + createStore as reduxCreateStore, + combineReducers, + applyMiddleware, + compose +} from 'redux'; +import * as H from 'history'; +import { connectRouter, routerMiddleware } from 'connected-react-router'; +import thunk from 'redux-thunk'; +import { UIReducer } from '../ui/reducers'; +import { OperationsReducer } from '../operations/reducers'; +import { CommandsReducer } from '../commands/reducers'; +import { TelemetriesReducer } from '../telemetries/reducers'; +import { PlansReducer } from '../plans/reducers'; +import { ViewsReducer } from '../views/reducers'; + +interface ExtendedWindow extends Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; +} +declare var window: ExtendedWindow; + +const composeReduxDevToolsEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + +export default function createStore(history: H.History) { + return reduxCreateStore( + combineReducers({ + router: connectRouter(history), + ui: UIReducer, + operation: OperationsReducer, + cmds: CommandsReducer, + tlms: TelemetriesReducer, + plans: PlansReducer, + views: ViewsReducer + }), + composeReduxDevToolsEnhancers( + applyMiddleware( + routerMiddleware(history), + thunk + )) + ) +} diff --git a/aspnetapp/WINGS/ClientApp/src/redux/telemetries/actions.ts b/aspnetapp/WINGS/ClientApp/src/redux/telemetries/actions.ts new file mode 100644 index 0000000..d33a18f --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/telemetries/actions.ts @@ -0,0 +1,26 @@ +import { TelemetryPacket, TelemetryPacketHistory } from "../../models"; + +export const UPDATE_LATEST_TELEMETRY = 'UPDATE_LATEST_TELEMETRY' as const; +export const UPDATE_TELEMETRY_HISTORY = 'UPDATE_TELEMETRY_HISTORY' as const; +export const ADD_TELEMETRY_HISTORY = 'ADD_TELEMETRY_HISTORY' as const; + +export const updateLatestTelemetriesAction = (telemetries: TelemetryPacket[]) => { + return { + type: UPDATE_LATEST_TELEMETRY, + payload: telemetries + }; +}; + +export const updateTelemetryHistoriesAction = (telemetryHistories: TelemetryPacketHistory[]) => { + return { + type: UPDATE_TELEMETRY_HISTORY, + payload: telemetryHistories + }; +}; + +export const addTelemetryHistoriesAction = (telemetries: TelemetryPacket[]) => { + return { + type: ADD_TELEMETRY_HISTORY, + payload: telemetries + }; +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/telemetries/reducers.ts b/aspnetapp/WINGS/ClientApp/src/redux/telemetries/reducers.ts new file mode 100644 index 0000000..31a89d4 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/telemetries/reducers.ts @@ -0,0 +1,56 @@ +import * as Actions from './actions'; +import * as OperationActions from '../operations/actions'; +import initialState from '../store/initialState'; + +type Actions = + | ReturnType + | ReturnType + | ReturnType + | ReturnType +; + +export const TelemetriesReducer = (state = initialState.tlms, action: Actions) => { + switch (action.type) { + case Actions.UPDATE_LATEST_TELEMETRY: + const tlmPackets = action.payload; + var tlms = state.latest; + tlmPackets.forEach(tlmPacket => { + tlms[tlmPacket.packetInfo.name] = tlmPacket.telemetries; + }) + return { + ...state, + latest: tlms + } + + case Actions.UPDATE_TELEMETRY_HISTORY: + const tlmPacketHistories = action.payload; + const tlmHistories = tlmPacketHistories.reduce((list, tlmPacketHistory) => ({ ...list, [tlmPacketHistory.packetInfo.name]: tlmPacketHistory.telemetryHistories }), {}) + return { + ...state, + history: tlmHistories + } + + case Actions.ADD_TELEMETRY_HISTORY: + const latestTlms = action.payload; + let tlmHstrs = state.history; + if (latestTlms != []){ + latestTlms.forEach(tlmPacket => { + if(tlmPacket.telemetries[0].telemetryValue.time != null) { + tlmHstrs[tlmPacket.packetInfo.name].forEach((element, index) => { + element.telemetryValues.push(tlmPacket.telemetries[index].telemetryValue); + }) + } + }) + } + return { + ...state, + history: tlmHstrs + } + + case OperationActions.LEAVE_OPERATION: + return initialState.tlms; + + default: + return state; + } +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/telemetries/selectors.ts b/aspnetapp/WINGS/ClientApp/src/redux/telemetries/selectors.ts new file mode 100644 index 0000000..8c4585a --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/telemetries/selectors.ts @@ -0,0 +1,14 @@ +import { createSelector } from 'reselect'; +import { RootState } from '../store/RootState'; + +const telemetriesSelector = (state: RootState) => state.tlms; + +export const getLatestTelemetries = createSelector( + [telemetriesSelector], + state => state.latest +); + +export const getTelemetryHistories = createSelector( + [telemetriesSelector], + state => state.history +); \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/redux/ui/actions.ts b/aspnetapp/WINGS/ClientApp/src/redux/ui/actions.ts new file mode 100644 index 0000000..3c4c67d --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/ui/actions.ts @@ -0,0 +1,29 @@ +export const START_LOADING = 'START_LOADING' as const; +export const END_LOADING = 'END_LOADING' as const; +export const OPEN_ERROR_DIALOG = 'OPEN_ERROR_DIALOG' as const; +export const CLOSE_ERROR_DIALOG = 'CLOSE_ERROR_DIALOG' as const; + +export const startLoadingAction = () => { + return { + type: START_LOADING + }; +}; + +export const endLoadingAction = () => { + return { + type: END_LOADING + }; +}; + +export const openErrorDialogAction = (message: string) => { + return { + type: OPEN_ERROR_DIALOG, + payload: message + }; +}; + +export const closeErrorDialogAction = () => { + return { + type: CLOSE_ERROR_DIALOG + }; +}; diff --git a/aspnetapp/WINGS/ClientApp/src/redux/ui/reducers.ts b/aspnetapp/WINGS/ClientApp/src/redux/ui/reducers.ts new file mode 100644 index 0000000..5cccc6a --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/ui/reducers.ts @@ -0,0 +1,46 @@ +import * as Actions from './actions'; +import initialState from '../store/initialState'; + +type Actions = + | ReturnType + | ReturnType + | ReturnType + | ReturnType +; + +export const UIReducer = (state = initialState.ui, action: Actions) => { + switch (action.type) { + case Actions.START_LOADING: + return { + ...state, + isLoading: true + }; + + case Actions.END_LOADING: + return { + ...state, + isLoading: false + }; + + case Actions.OPEN_ERROR_DIALOG: + return { + ...state, + errorDialog: { + open: true, + message: action.payload + } + }; + + case Actions.CLOSE_ERROR_DIALOG: + return { + ...state, + errorDialog: { + open: false, + message: "" + } + }; + + default: + return state; + } +} \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/redux/ui/selectors.ts b/aspnetapp/WINGS/ClientApp/src/redux/ui/selectors.ts new file mode 100644 index 0000000..87bbfd0 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/ui/selectors.ts @@ -0,0 +1,14 @@ +import { createSelector } from 'reselect'; +import { RootState } from '../store/RootState'; + +const uiSelector = (state: RootState) => state.ui; + +export const getIsLoading = createSelector( + [uiSelector], + state => state.isLoading +); + +export const getErrorDialogState = createSelector( + [uiSelector], + state => state.errorDialog +); diff --git a/aspnetapp/WINGS/ClientApp/src/redux/views/actions.ts b/aspnetapp/WINGS/ClientApp/src/redux/views/actions.ts new file mode 100644 index 0000000..d736879 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/views/actions.ts @@ -0,0 +1,111 @@ +import { TelemetryViewIndex, Layout, TelemetryView } from "../../models"; + +export const FETCH_VIEW_INDEXES = 'FETCH_VIEW_INDEXES' as const; +export const FETCH_LAYOUTS = 'FETCH_LAYOUTS' as const; +export const OPEN_VIEW = 'OPEN_VIEW' as const; +export const CLOSE_VIEW = 'CLOSE_VIEW' as const; +export const ACTIVATE_VIEW = 'ACTIVATE_VIEW' as const; +export const TEMP_STORE_VIEW = 'TEMP_STORE_VIEW' as const; +export const BACK_VIEW = 'BACK_VIEW' as const; +export const SELECTED_LAYOUT_COMMIT = 'SELECTED_LAYOUT_COMMIT' as const; +export const SELECT_TELEMETRY = 'SELECT_TELEMETRY' as const; +export const SET_TELEMETRY_TYPE_PACKET = 'SET_TELEMETRY_TYPE_PACKET' as const; +export const SET_TELEMETRY_TYPE_GRAPH = 'SET_TELEMETRY_TYPE_GRAPH' as const; + +export const fetchViewIndexesAction = (indexes: TelemetryViewIndex[]) => { + return { + type: FETCH_VIEW_INDEXES, + payload: indexes + }; +}; + +export const fetchLayoutsAction = (layouts: Layout[]) => { + return { + type: FETCH_LAYOUTS, + payload: layouts + }; +}; + +export const openViewAction = (block: number, id: string, content: any) => { + return { + type: OPEN_VIEW, + payload: { + block: block, + id: id, + content: content + } + }; +}; + +export const closeViewAction = (block: number, tab: number) => { + return { + type: CLOSE_VIEW, + payload: { + block: block, + tab: tab + } + }; +}; + +export const activateViewAction = (block: number, tab: number) => { + return { + type: ACTIVATE_VIEW, + payload: { + block: block, + tab: tab + } + } +}; + +export const tempStoreViewAction = (telemetryView: TelemetryView) => { + return { + type: TEMP_STORE_VIEW, + payload: telemetryView + } +}; + +export const backViewAction = () => { + return { + type: BACK_VIEW, + } +}; + +export const selectedLayoutCommitAction = (index: number) => { + return { + type: SELECTED_LAYOUT_COMMIT, + payload: index + } +}; + +export const selectTelemetryAction = (block: number, tlmname:string[]) => { + return { + type: SELECT_TELEMETRY, + payload: { + block: block, + tlmname: tlmname + } + } +}; + +export const setTelemetryTypePacketAction = (block: number, dataType:string) => { + return { + type: SET_TELEMETRY_TYPE_PACKET, + payload: { + block: block, + dataType: dataType + } + } +}; + +export const setTelemetryTypeGraphAction = (block: number, dataType:string, dataLength:string, ylabelMin:string, ylabelMax:string) => { + return { + type: SET_TELEMETRY_TYPE_GRAPH, + payload: { + block: block, + dataType: dataType, + dataLength: dataLength, + ylabelMin: ylabelMin, + ylabelMax: ylabelMax, + } + } +}; \ No newline at end of file diff --git a/aspnetapp/WINGS/ClientApp/src/redux/views/reducers.ts b/aspnetapp/WINGS/ClientApp/src/redux/views/reducers.ts new file mode 100644 index 0000000..07d3c26 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/views/reducers.ts @@ -0,0 +1,217 @@ +import * as Actions from './actions'; +import * as OperationActions from '../operations/actions'; +import initialState from '../store/initialState'; + +type Actions = + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType +; + +export const ViewsReducer = (state = initialState.views, action: Actions) => { + switch (action.type) { + case Actions.FETCH_VIEW_INDEXES: { + return { + ...state, + currentView: { + ...state.currentView, + allIndexes: action.payload + } + } + } + + case Actions.OPEN_VIEW: { + const { block, id, content } = action.payload; + const index = state.currentView.allIndexes.find(index => index.id === id); + return { + ...state, + currentView: { + ...state.currentView, + blocks: [ + ...state.currentView.blocks.slice(0,block), + { + tabs: [...state.currentView.blocks[block].tabs, index], + activeTab: state.currentView.blocks[block].tabs.length + }, + ...state.currentView.blocks.slice(block+1) + ], + contents: { + ...state.currentView.contents, + [id]: content + } + } + }; + } + + case Actions.CLOSE_VIEW: { + const { block, tab } = action.payload; + return { + ...state, + currentView: { + ...state.currentView, + blocks: [ + ...state.currentView.blocks.slice(0,block), + { + ...state.currentView.blocks[block], + tabs: [ + ...state.currentView.blocks[block].tabs.slice(0,tab), + ...state.currentView.blocks[block].tabs.slice(tab+1) + ] + }, + ...state.currentView.blocks.slice(block+1) + ] + } + }; + } + + case Actions.ACTIVATE_VIEW: { + const { block, tab } = action.payload; + return { + ...state, + currentView: { + ...state.currentView, + blocks: [ + ...state.currentView.blocks.slice(0,block), + { + ...state.currentView.blocks[block], + activeTab: tab + }, + ...state.currentView.blocks.slice(block+1) + ] + } + }; + } + + case Actions.TEMP_STORE_VIEW: { + const tempLayout = action.payload; + return { + ...state, + tempStoredView: { + allIndexes: tempLayout.allIndexes, + blocks: tempLayout.blocks, + contents: tempLayout.contents + } + } + } + + case Actions.BACK_VIEW: { + return { + ...state, + currentView: { + allIndexes: state.tempStoredView.allIndexes, + blocks: state.tempStoredView.blocks, + contents: state.tempStoredView.contents + } + } + } + + + case Actions.SELECTED_LAYOUT_COMMIT: { + const index = action.payload; + return { + ...state, + currentView: { + allIndexes: state.layoutList[index].telemetryView.allIndexes, + blocks: state.layoutList[index].telemetryView.blocks, + contents: state.layoutList[index].telemetryView.contents + } + } + } + + case Actions.SELECT_TELEMETRY: { + const { block, tlmname } = action.payload; + return { + ...state, + currentView: { + ...state.currentView, + blocks: [ + ...state.currentView.blocks.slice(0, block), + { + ...state.currentView.blocks[block], + tabs: [...state.currentView.blocks[block].tabs.slice(0, state.currentView.blocks[block].activeTab), + { + ...state.currentView.blocks[block].tabs[state.currentView.blocks[block].activeTab], + selectedTelemetries: tlmname + }, + ...state.currentView.blocks[block].tabs.slice(state.currentView.blocks[block].activeTab+1)] + }, + ...state.currentView.blocks.slice(block + 1) + ] + } + }; + } + + case Actions.SET_TELEMETRY_TYPE_PACKET: { + const { block, dataType } = action.payload; + return { + ...state, + currentView:{ + ...state.currentView, + blocks: [ + ...state.currentView.blocks.slice(0, block), + { + ...state.currentView.blocks[block], + tabs: [...state.currentView.blocks[block].tabs.slice(0, state.currentView.blocks[block].activeTab), + { + ...state.currentView.blocks[block].tabs[state.currentView.blocks[block].activeTab], + dataType: dataType + }, + ...state.currentView.blocks[block].tabs.slice(state.currentView.blocks[block].activeTab+1)] + }, + ...state.currentView.blocks.slice(block + 1) + ] + } + }; + } + + case Actions.SET_TELEMETRY_TYPE_GRAPH: { + const { block, dataType, dataLength, ylabelMin, ylabelMax } = action.payload; + return { + ...state, + currentView:{ + ...state.currentView, + blocks: [ + ...state.currentView.blocks.slice(0, block), + { + ...state.currentView.blocks[block], + tabs: [...state.currentView.blocks[block].tabs.slice(0, state.currentView.blocks[block].activeTab), + { + ...state.currentView.blocks[block].tabs[state.currentView.blocks[block].activeTab], + dataType: dataType, + dataLength: dataLength, + ylabelMin: ylabelMin, + ylabelMax: ylabelMax + }, + ...state.currentView.blocks[block].tabs.slice(state.currentView.blocks[block].activeTab+1)] + }, + ...state.currentView.blocks.slice(block + 1) + ] + } + }; + } + + case OperationActions.LEAVE_OPERATION: { + return initialState.views; + } + + case Actions.FETCH_LAYOUTS: { + const layouts = action.payload; + return { + ...state, + layoutList: layouts + }; + } + + default: + return state; + } +} diff --git a/aspnetapp/WINGS/ClientApp/src/redux/views/selectors.ts b/aspnetapp/WINGS/ClientApp/src/redux/views/selectors.ts new file mode 100644 index 0000000..5ba2669 --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/src/redux/views/selectors.ts @@ -0,0 +1,34 @@ +import { createSelector } from 'reselect'; +import { RootState } from '../store/RootState'; + +const viewsSelector = (state: RootState) => state.views; + +export const getAllIndexes = createSelector( + [viewsSelector], + state => state.currentView.allIndexes +); + +export const getPacketNames = createSelector( + [viewsSelector], + state => state.currentView.allIndexes.filter(index => index.type === "packet") +); + +export const getBlockInfos = createSelector( + [viewsSelector], + state => state.currentView.blocks +); + +export const getViewContents = createSelector( + [viewsSelector], + state => state.currentView.contents +); + +export const getViewLayout = createSelector( + [viewsSelector], + state => state.currentView +); + +export const getLayouts = createSelector( + [viewsSelector], + state => state.layoutList +); diff --git a/aspnetapp/WINGS/ClientApp/tsconfig.json b/aspnetapp/WINGS/ClientApp/tsconfig.json new file mode 100644 index 0000000..a273b0c --- /dev/null +++ b/aspnetapp/WINGS/ClientApp/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} diff --git a/aspnetapp/WINGS/Controllers/CommandController.cs b/aspnetapp/WINGS/Controllers/CommandController.cs new file mode 100644 index 0000000..9fc3b95 --- /dev/null +++ b/aspnetapp/WINGS/Controllers/CommandController.cs @@ -0,0 +1,160 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using WINGS.Models; +using WINGS.Services; +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace WINGS.Controllers +{ + [ApiController] + [Route("api/operations/{id}")] + public class CommandController : ControllerBase + { + private readonly ICommandService _commandService; + + public CommandController(ICommandService commandService) + { + _commandService = commandService; + } + + // GET: api/operations/f364../cmd + [HttpGet("cmd")] + public IActionResult GetAll(string id) + { + try + { + var commands = _commandService.GetAllCommand(id); + + return StatusCode(Status200OK, new { data = commands }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // POST: api/operations/f364../cmd + [HttpPost("cmd")] + public async Task Send(string id, [FromBody]JsonElement json) + { + var cmdStr = json.GetProperty("command").ToString(); + var command = JsonSerializer.Deserialize(cmdStr, new JsonSerializerOptions{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + + var commanderId = ""; + + var ack = await _commandService.SendCommandAsync(id, command, commanderId); + return StatusCode(Status200OK, new { ack = ack }); + } + + // POST: api/operations/f364../cmd/raw + [HttpPost("cmd/raw")] + public IActionResult SendRaw(string id, [FromBody]JsonElement json) + { + var bytesStr = json.GetProperty("data").ToString().Split(" "); + var num = bytesStr.Length; + if (num == 0) + { + return StatusCode(Status400BadRequest, new { message = "The command bytes are empty" }); + } + var packet = new byte[num]; + for (int i = 0; i < num; i++) + { + try + { + packet[i] = Byte.Parse(bytesStr[i].Remove(0,2), System.Globalization.NumberStyles.HexNumber); + } + catch + { + return StatusCode(Status400BadRequest, new { message = "Cannot parse to byte array" }); + } + } + _commandService.SendRawCommand(id, packet); + return StatusCode(Status200OK); + } + + // GET: api/operations/f364../cmd_fileline_log + [HttpGet("cmd_fileline/log")] + public IActionResult GetCmdFileLogHistory(string id) + { + try + { + var cmdFileLogHistory = _commandService.GetCmdLogHistory(id); + return StatusCode(Status200OK, new { data = cmdFileLogHistory }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // POST: api/operations/f364../cmd_fileline_log + [HttpPost("cmd_fileline_log")] + public async Task SendCmdFileLineLog(string id, [FromBody] JsonElement json) + { + var cmdFileLineStr = json.GetProperty("command_file_line_log").ToString(); + var commandFileLineLog = JsonSerializer.Deserialize(cmdFileLineStr, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + var commanderName = ""; + var ack = await _commandService.AddCmdFileLineLog(id, commandFileLineLog, commanderName); + return StatusCode(Status200OK, new { ack = ack }); + } + + // GET: api/operations/f364../cmd_plans + [HttpGet("cmd_plans")] + public IActionResult GetCommandFileIndexes(string id) + { + try + { + var indexes = _commandService.GetCommandFileIndexes(id); + return StatusCode(Status200OK, new { data = indexes }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // PUT: api/operations/f364../cmd_plans + [HttpPut("cmd_plans")] + public async Task ReconfigureCommandFileIndexes(string id) + { + try + { + await _commandService.ReconfigureCommandFileAsync(id); + return StatusCode(Status200OK); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // GET: api/operations/f364../cmd_plans/0/5 + [HttpGet("cmd_plans/{cmd_file_info_index}/{file_id}")] + public async Task GetCommandFile(string id, int cmdFileInfoIndex, int fileId) + { + if (fileId < 0) + { + return StatusCode(Status400BadRequest, new { message = "The file id must be an integer greater than or equal to 0" }); + } + try + { + var file = await _commandService.GetCommandFileAsync(id, cmdFileInfoIndex, fileId); + return StatusCode(Status200OK, new { data = file }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + } +} diff --git a/aspnetapp/WINGS/Controllers/ComponentController.cs b/aspnetapp/WINGS/Controllers/ComponentController.cs new file mode 100644 index 0000000..c668df8 --- /dev/null +++ b/aspnetapp/WINGS/Controllers/ComponentController.cs @@ -0,0 +1,97 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using WINGS.Data; +using WINGS.Models; +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace WINGS.Controllers +{ + [ApiController] + [Route("api/components")] + public class ComponentController : ControllerBase + { + private readonly ApplicationDbContext _dbContext; + + public ComponentController(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + // GET: api/components + [HttpGet] + public async Task Get() + { + var components = await _dbContext.Components.ToListAsync(); + return StatusCode(Status200OK, new { data = components }); + } + + // POST: api/components + [HttpPost] + public async Task Create(Component component) + { + try + { + component.Id = Guid.NewGuid().ToString("d"); + _dbContext.Components.Add(component); + await _dbContext.SaveChangesAsync(); + return StatusCode(Status201Created); + } + catch + { + return StatusCode(Status500InternalServerError, new { message = "Cannot create new entity" }); + } + } + + // PUT: api/components/5 + [HttpPut("{id}")] + public async Task Update(string id, Component component) + { + if (id != component.Id) + { + return StatusCode(Status400BadRequest, new { message = "The id doesn't match" }); + } + + try + { + _dbContext.Entry(component).State = EntityState.Modified; + await _dbContext.SaveChangesAsync(); + return StatusCode(Status200OK); + } + catch + { + if (!ComponentExists(id)) + { + return StatusCode(Status404NotFound, new { message = "The component is not found" }); + } + return StatusCode(Status500InternalServerError, new { message = "Cannot update entity" }); + } + } + + // DELETE: api/components/5 + [HttpDelete("{id}")] + public async Task Delete(string id) + { + try + { + var component = await _dbContext.Components.FindAsync(id); + _dbContext.Components.Remove(component); + await _dbContext.SaveChangesAsync(); + return StatusCode(Status204NoContent); + } + catch + { + if (!ComponentExists(id)) + { + return StatusCode(Status404NotFound, new { message = "The component is not found" }); + } + return StatusCode(Status500InternalServerError, new { message = "Cannot delete entity" }); + } + } + + private bool ComponentExists(string id) => + _dbContext.Components.Any(o => o.Id == id); + } +} diff --git a/aspnetapp/WINGS/Controllers/HistoryController.cs b/aspnetapp/WINGS/Controllers/HistoryController.cs new file mode 100644 index 0000000..f89504d --- /dev/null +++ b/aspnetapp/WINGS/Controllers/HistoryController.cs @@ -0,0 +1,193 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using WINGS.Services; +using WINGS.Models; +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace WINGS.Controllers +{ + [ApiController] + [Route("api/operations")] + public class HistoryController : ControllerBase + { + private readonly IOperationService _operationService; + private readonly ICommandService _commandService; + private readonly ITelemetryService _telemetryService; + + public HistoryController(IOperationService operationService, + ICommandService commandService, + ITelemetryService telemetryService) + { + _operationService = operationService; + _commandService = commandService; + _telemetryService = telemetryService; + } + + // GET: api/operations/history?page=1&size=30&search=aaa + [HttpGet("history")] + public async Task Get(int page = 1, int size = 30, string search = "") + { + Pagination operations = await _operationService.GetOperationHistoryAsync(page, size, search); + return StatusCode(Status200OK, operations); + } + + // PUT: api/operations/f364../history + [HttpPut("{id}/history")] + public async Task Update(string id, Operation operation) + { + if (id != operation.Id) + { + return StatusCode(Status400BadRequest, new { message = "The id doesn't match" }); + } + try + { + await _operationService.UpdateOperationHistoryAsync(operation); + return StatusCode(Status200OK); + } + catch (IllegalContextException ex) + { + return StatusCode(Status400BadRequest, new { message = ex.Message }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + catch (ResourceUpdateException ex) + { + return StatusCode(Status500InternalServerError, new { message = ex.Message }); + } + } + + // DELETE: api/operations/f364../history + [HttpDelete("{id}/history")] + public async Task DeleteHistory(string id) + { + try + { + await _operationService.DeleteOperationHistoryAsync(id); + return StatusCode(Status204NoContent); + } + catch (IllegalContextException ex) + { + return StatusCode(Status400BadRequest, new { message = ex.Message }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + catch (ResourceDeleteException ex) + { + return StatusCode(Status500InternalServerError, new { message = ex.Message }); + } + } + + // GET: api/operations/f364../history/cmd_logs + [HttpGet("{id}/history/cmd_logs")] + public async Task DownloadCommandLog(string id) + { + try + { + var fs = _commandService.GetCommandLogStream(id); + var operation = await _operationService.GetOperationByIdAsync(id); + var fileName = operation.PathNumber + "_CmdLog.csv"; + return File(fs, "text/csv", fileName); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // GET: api/operations/f364../history/cmdfile_logs + [HttpGet("{id}/history/cmdfile_logs")] + public async Task DownloadCommandFileLog(string id) + { + try + { + var fs = _commandService.GetCommandFileLogStream(id); + var operation = await _operationService.GetOperationByIdAsync(id); + var fileName = operation.PathNumber + "_CmdFileLog.csv"; + return File(fs, "text/csv", fileName); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // GET: api/operations/f364../history/tlm_packets + [HttpGet("{id}/history/tlm_packets")] + public IActionResult GetTelemetryPacketsWithData(string id) + { + try + { + var packets = _telemetryService.GetPacketsWithData(id); + return StatusCode(Status200OK, new { data = packets }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // GET: api/operations/f364../history/record_tlm_packets + [HttpGet("{id}/history/record_tlm_packets")] + public IActionResult GetRecordTelemetryPacketsWithData(string id) + { + try + { + var packets = _telemetryService.GetRecordPacketsWithData(id); + return StatusCode(Status200OK, new { data = packets }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // GET: api/operations/f364../history/tlm_logs?packet_name=aaa,bbb,ccc + [HttpGet("{id}/history/tlm_logs")] + public async Task DownloadTelemetryLog(string id, string packet_name) + { + if (packet_name == null) + { + return StatusCode(Status400BadRequest, new { message = "The packet name must not be null" }); + } + var packetNames = new List(packet_name.Split(",")); + try + { + var zs = _telemetryService.GetTelemetryLogStream(id, packetNames); + var operation = await _operationService.GetOperationByIdAsync(id); + string fileName = operation.PathNumber + "_TlmLog.zip"; + return File(zs, "application/zip", fileName); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // GET: api/operations/f364../history/record_tlm_logs?packet_name=aaa,bbb,ccc + [HttpGet("{id}/history/record_tlm_logs")] + public async Task DownloadRecordTelemetryLog(string id, string packet_name) + { + if (packet_name == null) + { + return StatusCode(Status400BadRequest, new { message = "The packet name must not be null" }); + } + var packetNames = new List(packet_name.Split(",")); + try + { + var zs = _telemetryService.GetRecordTelemetryLogStream(id, packetNames); + var operation = await _operationService.GetOperationByIdAsync(id); + string fileName = operation.PathNumber + "_RecordTlmLog.zip"; + return File(zs, "application/zip", fileName); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + } +} diff --git a/aspnetapp/WINGS/Controllers/LayoutController.cs b/aspnetapp/WINGS/Controllers/LayoutController.cs new file mode 100644 index 0000000..9753f34 --- /dev/null +++ b/aspnetapp/WINGS/Controllers/LayoutController.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using WINGS.Models; +using WINGS.Services; +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace WINGS.Controllers +{ + [ApiController] + [Route("api/operations/{id}")] + public class LayoutController : ControllerBase + { + private readonly ILayoutService _layoutService; + + public LayoutController(ILayoutService layoutService) + { + _layoutService = layoutService; + } + + // GET: api/operations/f364../lyt : fetch layout + [HttpGet("lyt")] + public IActionResult GetAll(string id) + { + try + { + var layouts = _layoutService.GetAllLayout(id); + return StatusCode(Status200OK, new { data = layouts }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + // POST: api/operations/f364../lyt + [HttpPost("lyt")] + public async Task Save(string id, Layout layout) + { + var name = layout.name; + var lytStr = JsonSerializer.Serialize(new Layout { telemetryView = layout.telemetryView }, new JsonSerializerOptions{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + var ack = await _layoutService.SaveLayoutAsync(id, name, lytStr); + return StatusCode(Status200OK, new { ack = ack }); + } + + // PUT: api/operations/f364../lyt + [HttpPut("lyt/{index}/{name}")] + public async Task Rename(string id, string name, int index) + { + var ack = await _layoutService.RenameLayoutAsync(id, name, index); + return StatusCode(Status200OK, new { ack = ack }); + } + + // DELETE: api/operations/f364../lyt + [HttpDelete("lyt/{name}")] + public async Task Delete(string id, string name) + { + var ack = await _layoutService.DeleteLayoutAsync(id, name); + return StatusCode(Status200OK, new { ack = ack }); + } + } +} diff --git a/aspnetapp/WINGS/Controllers/OperationController.cs b/aspnetapp/WINGS/Controllers/OperationController.cs new file mode 100644 index 0000000..dad208e --- /dev/null +++ b/aspnetapp/WINGS/Controllers/OperationController.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; +using WINGS.Services; +using WINGS.Models; +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace WINGS.Controllers +{ + [ApiController] + [Route("api/operations")] + public class OperationController : ControllerBase + { + private readonly IOperationService _operationService; + private readonly ICommandService _commandService; + private readonly ITelemetryService _telemetryService; + private readonly ILayoutService _layoutService; + private readonly ITlmCmdFileConfigBuilder _configBuilder; + + public OperationController(IOperationService operationService, + ICommandService commandService, + ITelemetryService telemetryService, + ILayoutService layoutService, + ITlmCmdFileConfigBuilder configBuilder) + { + _operationService = operationService; + _commandService = commandService; + _telemetryService = telemetryService; + _layoutService = layoutService; + _configBuilder = configBuilder; + } + + // GET: api/operations + [HttpGet] + public IActionResult Get() + { + var operations = _operationService.GetCurrentOperations(); + return StatusCode(Status200OK, new { data = operations }); + } + + // POST: api/operations + [HttpPost] + public async Task Start([FromBody] JsonElement json) + { + if (!json.TryGetProperty("operation", out JsonElement operationJson)) + { + return StatusCode(Status400BadRequest, new { message = "Plese set operation to json file" }); + } + var operationStr = operationJson.ToString(); + var operation = JsonSerializer.Deserialize(operationStr, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + try + { + var newOperation = await _operationService.StartOperationAsync(operation); + var config = await _configBuilder.Build(newOperation.Id); + Task cmdDbTask = Task.Run(() => _commandService.ConfigureCommandDbAsync(newOperation, config)); + Task tlmDbTask = Task.Run(() => _telemetryService.ConfigureTelemetryDbAsync(newOperation, config)); + Task lytDbTask = Task.Run(() => _layoutService.ConfigureLayoutAsync(newOperation, config)); + Task cmdFileTask = Task.Run(() => _commandService.ConfigureCommandFileAsync(newOperation, config)); + _commandService.ConfigureCommandFileLog(newOperation); + + if (!cmdDbTask.Result || !tlmDbTask.Result || !cmdFileTask.Result) + { + await _operationService.CancelOperationAsync(newOperation.Id); + return StatusCode(Status500InternalServerError); + } + return StatusCode(Status201Created); + } + catch (IllegalContextException ex) + { + return StatusCode(Status400BadRequest, new { message = ex.Message }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + catch (ResourceCreateException ex) + { + return StatusCode(Status500InternalServerError, new { message = ex.Message }); + } + } + + // DELETE: api/operations/f364.. + [HttpDelete("{id}")] + public async Task Delete(string id) + { + try + { + await _operationService.StopOperationAsync(id); + _commandService.RemoveCommandFileIndexes(id); + _layoutService.RemoveLayouts(id); + return StatusCode(Status204NoContent); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + } +} diff --git a/aspnetapp/WINGS/Controllers/TelemetryController.cs b/aspnetapp/WINGS/Controllers/TelemetryController.cs new file mode 100644 index 0000000..edcf1ab --- /dev/null +++ b/aspnetapp/WINGS/Controllers/TelemetryController.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; +using WINGS.Services; +using WINGS.Models; +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace WINGS.Controllers +{ + [ApiController] + [Route("api/operations/{id}")] + public class TelemetryController : ControllerBase + { + private readonly ITelemetryService _telemetryService; + + public TelemetryController(ITelemetryService telemetryService) + { + _telemetryService = telemetryService; + } + + // GET: api/operations/f364../tlm + [HttpGet("tlm")] + public IActionResult GetLatestTelemetry(string id, [FromQuery] string refTlmTime) + { + try + { + var latestTelemetry = _telemetryService.GetLatestTelemetry(id, refTlmTime); + + return StatusCode(Status200OK, new { data = latestTelemetry.LatestTelemetryPackets, latestTlmTime = latestTelemetry.LatestTelemetryTime }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + + // GET: api/operations/f364../tlm_history + [HttpGet("tlm_history")] + public IActionResult GetTelemetryHistory(string id) + { + try + { + var tlmPacketHistories = _telemetryService.GetTelemetryHistory(id); + + return StatusCode(Status200OK, new { data = tlmPacketHistories }); + } + catch (ResourceNotFoundException ex) + { + return StatusCode(Status404NotFound, new { message = ex.Message }); + } + } + } +} diff --git a/aspnetapp/WINGS/Data/ApplicationDbContext.cs b/aspnetapp/WINGS/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..d665f9f --- /dev/null +++ b/aspnetapp/WINGS/Data/ApplicationDbContext.cs @@ -0,0 +1,35 @@ +using WINGS.Models; +using Microsoft.EntityFrameworkCore; + +namespace WINGS.Data +{ + public class ApplicationDbContext : DbContext + { + public DbSet Operations { get; set; } + public DbSet Components { get; set; } + public DbSet CommandLogs { get; set; } + + public ApplicationDbContext( + DbContextOptions options + ) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(e => + { + e.Property(o => o.FileLocation) + .HasConversion(); + e.HasOne(o => o.Component) + .WithMany() + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity() + .HasKey(c => new { c.OperationId, c.SentAt }); + + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/aspnetapp/WINGS/Data/CommandDbRepository.cs b/aspnetapp/WINGS/Data/CommandDbRepository.cs new file mode 100644 index 0000000..203b608 --- /dev/null +++ b/aspnetapp/WINGS/Data/CommandDbRepository.cs @@ -0,0 +1,128 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using WINGS.Models; +using WINGS.Library; +using System.Text; + +namespace WINGS.Data +{ + /// + /// Provides methods for reading command db files + /// + public class CommandDbRepository : IDbRepository + { + private readonly IWebHostEnvironment _env; + + public CommandDbRepository(IWebHostEnvironment env) + { + _env = env; + } + + /// + /// Reads the command db file defined in config and parses to the Command objects + /// + /// Config of the db file + /// Returns an enumerable collection of command definitions that are read from command db file + /// Undefined config of the file + public async Task> LoadAllFilesAsync(TlmCmdFileConfig config) + { + var commandDb = new List(); + + foreach (var c in config.CmdDBInfo) + { + var filePaths = await GetDbFilePathsAsync(config.Location, c); + foreach (var filePath in filePaths) + { + commandDb.AddRange(await LoadFileAsync(config.Location, c, filePath)); + } + } + return commandDb; + } + + private async Task> LoadFileAsync(TlmCmdFileLocation location, TlmCmdFileLocationInfo cmdDBInfo, string filePath) + { + var commandDb = new List(); + var reader = await GetDbFileReaderAsync(location, cmdDBInfo, filePath); + using (var parser = new TextFieldParser(reader)) + { + parser.SetDelimiters(","); + parser.HasFieldsEnclosedInQuotes = true; + parser.TrimWhiteSpace = true; + string ComponentName = ""; + + while (!parser.EndOfData) + { + var cols = parser.ReadFields(); + if (parser.LineNumber == 1) { ComponentName = cols[0]; continue; } + else if (parser.LineNumber < 5) { continue; } + if (cols.All(x => x == "")) { break; } + if (cols[0] != "" && cols[0][0] == '*') + { + continue; + } + + var numParam = Convert.ToInt32(cols[4]); + var Params = new List(); + for (var i = 0; i < numParam; i++) + { + if (cols[2 * i + 5] == "raw" && i != numParam - 1) + { + throw new FormatException("The raw parameter should be the last one."); + } + else + { + Params.Add(new CommandParam() + { + Type = cols[2 * i + 5], + Description = cols[2 * i + 6] + }); + } + } + commandDb.Add(new Command() + { + Component = ComponentName, + ExecType = CmdExecType.RT, + ExecTime = 0, + Name = cols[1], + Code = cols[3], + Target = cols[2], + Params = Params, + IsDanger = cols[17] == "danger", + IsViaMobc = false, + IsRestricted = cols[18] == "restricted", + Description = cols[19] + }); + } + } + return commandDb; + } + + private Task> GetDbFilePathsAsync(TlmCmdFileLocation location, TlmCmdFileLocationInfo cmdDBInfo) + { + switch (location) + { + case TlmCmdFileLocation.Local: + string dirPath = Path.Combine(_env.ContentRootPath, cmdDBInfo.DirPath, "cmddb"); + return Task.FromResult(Directory.EnumerateFiles(dirPath, "*CMD_DB.csv", SearchOption.TopDirectoryOnly)); + default: + throw new ArgumentException("Undefined file location"); + } + } + + private Task GetDbFileReaderAsync(TlmCmdFileLocation location, TlmCmdFileLocationInfo cmdDBInfo, string filePath) + { + switch (location) + { + case TlmCmdFileLocation.Local: + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + return Task.FromResult(new StreamReader(filePath, Encoding.GetEncoding("shift-jis"))); + default: + throw new ArgumentException("Undefined file location"); + } + } + } +} diff --git a/aspnetapp/WINGS/Data/CommandFileLogRepository.cs b/aspnetapp/WINGS/Data/CommandFileLogRepository.cs new file mode 100644 index 0000000..63efdcd --- /dev/null +++ b/aspnetapp/WINGS/Data/CommandFileLogRepository.cs @@ -0,0 +1,164 @@ +using System; +using System.Linq; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using WINGS.Models; + +namespace WINGS.Data +{ + /// + /// Provides methods for handling csv CommandFile logs + /// + public class CommandFileLogRepository : ICommandFileLogRepository + { + private readonly IWebHostEnvironment _env; + + public CommandFileLogRepository(IWebHostEnvironment env) + { + _env = env; + } + + /// + /// Add the CommandFile log to csv files per packet + /// + /// Operation id + /// CommandFile packet to save the logs + public async Task AddHistoryAsync(string opid, CommandFileLineLog command_file_line_log, string commanderName) + { + string filePath = Path.Combine(_env.ContentRootPath, "Logs", opid, "cmdfilelog", "cmdfilelog.csv"); + var sb = new StringBuilder(); + sb.Append(DateTime.Now + ","); + sb.Append(commanderName + ","); + var cmdFileTxtTmp = CommandFileLineToText(command_file_line_log.Request); + sb.Append(((cmdFileTxtTmp.IndexOf(",") > -1 || cmdFileTxtTmp.IndexOf("�C") > -1) ? "\""+ cmdFileTxtTmp + "\"" : cmdFileTxtTmp) + ","); + sb.Append(command_file_line_log.Status.Success? "Success": "Error"); + sb.Append("\r\n"); + using (var sw = new StreamWriter(filePath, true, Encoding.UTF8)) + { + await sw.WriteAsync(sb.ToString()); + } + } + + public List GetCmdLogHistory(string opid) + { + string fileName = Path.Combine(_env.ContentRootPath, "Logs", opid, "cmdfilelog", "cmdfilelog.csv"); + FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + List cmdLogHistory = new List(); + var sr = new StreamReader(fs); + string[] cols; + while (!sr.EndOfStream) + { + cols = sr.ReadLine().Split(","); + for (int i = 0; i < (cols.Count()+1)%4; i++) + { + if (cols[i*4] == "Time"){ + continue; + } + else{ + CommandFileLineLogs commandFileLine = new CommandFileLineLogs(); + commandFileLine.Time = cols[i*4]; + commandFileLine.Commander = cols[i*4+1]; + commandFileLine.Content = cols[i*4+2]; + commandFileLine.Status = cols[i*4+3]; + cmdLogHistory.Add(commandFileLine); + } + } + } + return cmdLogHistory; + } + + /// + /// Create new files and write headers to initialize log files + /// + /// Operation id + public void InitializeLogFiles(string opid) + { + string dirPath = Path.Combine(_env.ContentRootPath, "Logs", opid, "cmdfilelog"); + Directory.CreateDirectory(dirPath); + string filePath = Path.Combine(dirPath, "cmdfilelog.csv"); + var sb = new StringBuilder(); + sb.Append("Time,CommanderName,CommandLine,Status\r\n"); + using (var sw = new StreamWriter(filePath, false, Encoding.UTF8)) + { + sw.Write(sb.ToString()); + } + } + + /// + /// Returns a stream of the specified csv file + /// + /// Operation id + public Stream GetLogFileStream(string opid) + { + string filePath = Path.Combine(_env.ContentRootPath, "Logs", opid, "cmdfilelog", "cmdfilelog.csv"); + return File.OpenRead(filePath); + } + + private string CommandFileLineToText(CommandFileLine command_file_line_log) + { + var sb = new StringBuilder(); + switch (command_file_line_log.Type) + { + case "comment": + return command_file_line_log.Body.ToString(); + case "command": + Command cmd_tmp = JsonSerializer.Deserialize(command_file_line_log.Body.ToString(), new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + switch (cmd_tmp.ExecType) + { + case CmdExecType.RT: + sb.Append("RT." + cmd_tmp.Name); + break; + case CmdExecType.TL: + sb.Append("TL." + cmd_tmp.Name + " " + cmd_tmp.ExecTime); + break; + case CmdExecType.BL: + sb.Append("BL." + cmd_tmp.Name + " " + cmd_tmp.ExecTime); + break; + case CmdExecType.UTL: + sb.Append("UTL." + cmd_tmp.Name + " " + cmd_tmp.ExecTime); + break; + default: + return ""; + } + foreach (var param in cmd_tmp.Params) + { + sb.Append(" " + param.Value); + } + if (!String.IsNullOrEmpty(command_file_line_log.InlineComment)) + { + sb.Append(" " + command_file_line_log.InlineComment); + } + return sb.ToString(); + case "control": + switch (command_file_line_log.Method) + { + case "wait_sec": + sb.Append(command_file_line_log.Method + " " + command_file_line_log.Body.GetProperty("time").ToString()); + break; + case "call": + sb.Append(command_file_line_log.Method + " " + command_file_line_log.Body.GetProperty("fileName").ToString()); + break; + case "check_value": + sb.Append(command_file_line_log.Method + " " + command_file_line_log.Body.GetProperty("variable").ToString() + " " + command_file_line_log.Body.GetProperty("compare").ToString() + " " + command_file_line_log.Body.GetProperty("value").ToString()); + break; + } + if (!String.IsNullOrEmpty(command_file_line_log.InlineComment)) + { + sb.Append(" " + command_file_line_log.InlineComment); + } + return sb.ToString(); + default: + return ""; + } + } + } +} diff --git a/aspnetapp/WINGS/Data/CommandFileRepository.cs b/aspnetapp/WINGS/Data/CommandFileRepository.cs new file mode 100644 index 0000000..c8d9991 --- /dev/null +++ b/aspnetapp/WINGS/Data/CommandFileRepository.cs @@ -0,0 +1,587 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Globalization; +using Microsoft.AspNetCore.Hosting; +using WINGS.Models; +using WINGS.Library; + +namespace WINGS.Data +{ + /// + /// Provides methods for reading command plan files + /// + public class CommandFileRepository : ICommandFileRepository + { + private readonly IWebHostEnvironment _env; + + public CommandFileRepository(IWebHostEnvironment env) + { + _env = env; + } + + /// + /// Lists all command plan files that exist in the location defined in config + /// + /// Config of the files + /// Returns a list of the command file indexes + /// Undefined config of the files + public async Task> LoadCommandFileIndexesAsync(TlmCmdFileConfig config) + { + var indexes = new List(); + int idx = 0; + foreach (var c in config.CmdFileInfo) + { + var filePaths = await GetCommandFilePathsAsync(config.Location, c); + foreach (var item in filePaths.Select((v, i) => new { v, i })) + { + var index = new CommandFileIndex() + { + FileId = item.i, + Name = Path.GetFileNameWithoutExtension(item.v), + FilePath = item.v, + CmdFileInfoIndex = idx + }; + indexes.Add(index); + } + idx++; + } + return indexes; + } + + /// + /// Reads the contents of the command file specified by the index and parses to CommandFile object + /// + /// Config of the file + /// Index of the file + /// Definition of the commands + /// Returns a CommandFile object of the specified file + /// Undefined config of the file + public async Task LoadCommandFileAsync(TlmCmdFileConfig config, CommandFileIndex index, List commandDb) + { + var reader = await GetCommandFileReaderAsync(config.Location, config.CmdFileInfo[index.CmdFileInfoIndex], index.FilePath); + var content = new List(); + string line; + while ((line = reader.ReadLine()) != null) + { + var newContent = new CommandFileLine(); + + if (String.IsNullOrWhiteSpace(line)) + { + newContent.Type = "comment"; + newContent.Body = ""; + content.Add(newContent); + continue; + } + + StopFlagCheck (ref line, newContent); + + CommentCheck (ref line, newContent); + if (newContent.Type != null) + { + content.Add(newContent); + continue; + } + + ControlCheck (ref line, newContent); + if (newContent.Type != null) + { + content.Add(newContent); + continue; + } + + CommandCheck(ref line, newContent, commandDb); + if (newContent.Type != null) + { + content.Add(newContent); + continue; + } + + if (newContent.SyntaxError == false) + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "Type is null."; + } + + content.Add(newContent); + } + + return new CommandFile() + { + Index = index, + Content = content + }; + } + + private Task> GetCommandFilePathsAsync(TlmCmdFileLocation location, TlmCmdFileLocationInfo cmdFileInfo) + { + switch (location) + { + case TlmCmdFileLocation.Local: + string dirPath = Path.Combine(_env.ContentRootPath, cmdFileInfo.DirPath, "cmdplan"); + if (Directory.Exists(dirPath)) + { + return Task.FromResult(Directory.EnumerateFiles(dirPath, "*.ops", SearchOption.AllDirectories)); + } + else + { + throw new ArgumentException("Undefined file location"); + } + default: + throw new ArgumentException("Undefined file location"); + } + } + + private Task GetCommandFileReaderAsync(TlmCmdFileLocation location, TlmCmdFileLocationInfo cmdFileInfo, string filePath) + { + switch (location) + { + case TlmCmdFileLocation.Local: + return Task.FromResult(new StreamReader(filePath)); + default: + throw new ArgumentException("Undefined file location"); + } + } + + private void StopFlagCheck(ref string line, CommandFileLine newContent) + { + line = line.Trim(); + if (line[0] == '.') + { + newContent.StopFlag = true; + line = line.Substring(1); + } + } + + private void CommentCheck(ref string line, CommandFileLine newContent) + { + // comment & inline comment + var hashindex = line.IndexOf('#'); + switch (hashindex) + { + case -1: + break; + case 0: + newContent.Type = "comment"; + newContent.Body = line; + break; + default: + newContent.InlineComment = line.Substring(hashindex); + line = line.Remove(hashindex).Trim(); + break; + } + } + + private void ControlCheck(ref string line, CommandFileLine newContent) + { + string[] splitlinearray = line.Split(new string[] { " ", " " }, StringSplitOptions.RemoveEmptyEntries); + var splitlinelist = new List(splitlinearray); + switch (splitlinelist[0].ToLower()) + { + case "call": + newContent.Type = "control"; + newContent.Method = splitlinelist[0].ToLower(); + splitlinelist.RemoveAt(0); + CallParse(splitlinelist, newContent); + break; + case "modechk": + newContent.Type = "control"; + newContent.Method = splitlinelist[0].ToLower(); + splitlinelist.RemoveAt(0); + ModechkParse(splitlinelist, newContent); + break; + case "wait_sec": + newContent.Type = "control"; + newContent.Method = splitlinelist[0].ToLower(); + splitlinelist.RemoveAt(0); + WaitsecParse(splitlinelist, newContent); + break; + case "check_value": + newContent.Type = "control"; + newContent.Method = splitlinelist[0].ToLower(); + splitlinelist.RemoveAt(0); + CheckValueParse(splitlinelist, newContent); + break; + } + } + + + private void CallParse(List splitlinelist, CommandFileLine newContent) + { + switch(splitlinelist.Count) + { + case 1: + if(splitlinelist[0].EndsWith(".ops",StringComparison.OrdinalIgnoreCase)) + { + newContent.Body = new { + FileName = splitlinelist[0].TrimEnd('.', 'o', 'p', 's') + }; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "Method \"call\" : end with *.ops"; + } + break; + default: + newContent.SyntaxError = true; + newContent.ErrorMessage = "Method \"call\" : wrong number of data"; + break; + } + } + + private void ModechkParse(List splitlinelist, CommandFileLine newContent) + { + switch(splitlinelist.Count) + { + case 1: + if(splitlinelist[0].EndsWith(".mod",StringComparison.OrdinalIgnoreCase)) + { + newContent.Body = new { + FileName = splitlinelist[0].TrimEnd('.', 'm', 'o', 'd') + }; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "Method \"modechk\" : end with *.mod"; + } + break; + default: + newContent.SyntaxError = true; + newContent.ErrorMessage = "Method \"modechk\" : wrong number of data"; + break; + } + } + + private void WaitsecParse(List splitlinelist, CommandFileLine newContent) + { + switch(splitlinelist.Count) + { + case 1: + float sec; + if(float.TryParse(splitlinelist[0], out sec)) + { + newContent.Body = new { + time = sec + }; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "Method \"wait_sec\" : type error 'time'"; + } + break; + default: + newContent.SyntaxError = true; + newContent.ErrorMessage = "Method \"wait_sec\" : wrong number of data"; + break; + } + } + + private void CheckValueParse(List splitlinelist, CommandFileLine newContent) + { + switch (splitlinelist.Count) + { + case 3: + if (splitlinelist[1] == "==" || splitlinelist[1] == "<=" || splitlinelist[1] == ">=" || splitlinelist[1] == "<" || splitlinelist[1] == ">" || splitlinelist[1] == "!=") + { + newContent.Body = new + { + variable = splitlinelist[0], + compare = splitlinelist[1], + value = splitlinelist[2] + }; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "Method \"check_value\" : wrong number of data"; + } + break; + default: + newContent.SyntaxError = true; + newContent.ErrorMessage = "Method \"check_value\" : wrong number of data"; + break; + } + } + + private void CommandCheck(ref string line, CommandFileLine newContent, List commandDb) + { + // command + // ex. Cmd_BCT_SET_BLOCK_POSITION 15 0 + // ex. OBC_TL.Cmd_BCT_SET_BLOCK_POSITION 100 15 0 + string[] splitlinearray = line.Split(new string[] { " ", " " }, StringSplitOptions.RemoveEmptyEntries); + var splitlinelist = new List(splitlinearray); + if (splitlinelist[0].IndexOf(".") != -1) + { + CommandExeParse(splitlinelist, newContent, commandDb); + } + else + { + CommandParse(splitlinelist, newContent, commandDb); + } + } + + private void CommandExeParse(List splitlinelist, CommandFileLine newContent, List commandDb) + { + var dotIndex = splitlinelist[0].IndexOf('.'); + CmdExecType execType = CmdExecType.RT; + var component = ""; + var isViaMobc = false; + if (dotIndex > 3) + { + var mobcIndex = splitlinelist[0].IndexOf("MOBC_"); + if (mobcIndex >= 0 && dotIndex > 11) + { + var afterMobcLine = splitlinelist[0].Substring(mobcIndex + 8); + component = afterMobcLine.Substring(0, afterMobcLine.IndexOf('_')); + Enum.TryParse(splitlinelist[0].Substring(mobcIndex + 5, dotIndex - mobcIndex - 5), out execType); + isViaMobc = true; + } + else + { + component = splitlinelist[0].Substring(0, splitlinelist[0].IndexOf('_')); + Enum.TryParse(splitlinelist[0].Substring(5, dotIndex - 5), out execType); + } + } + var cmdBodyBuf = commandDb.FirstOrDefault(c => c.Name == splitlinelist[0].Substring(dotIndex + 1) && c.Component == component); + if (cmdBodyBuf == null) + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"CommandExe\" : Command not found. Check the first word in this line."; + return; + } + newContent.Body = cmdBodyBuf.Clone(); + newContent.Body.IsViaMobc = isViaMobc; + newContent.Body.ExecType = execType; + newContent.Type = "command"; + splitlinelist.RemoveAt(0); + if (newContent.Body.ExecType == CmdExecType.TL || newContent.Body.ExecType == CmdExecType.BL || newContent.Body.ExecType == CmdExecType.UTL) + { + uint execTime; + if (uint.TryParse(splitlinelist[0], out execTime)) + { + newContent.Body.ExecTime = execTime; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"CommandExe\" : Exetype is 'TL' or 'BL' or 'UTL'. The first parameter should be uint."; + return; + } + splitlinelist.RemoveAt(0); + } + ParamsTypeCheck(splitlinelist, newContent); + } + + private void CommandParse(List splitlinelist, CommandFileLine newContent, List commandDb) + { + var cmdBodyBuf = commandDb.FirstOrDefault(c => c.Name == splitlinelist[0]); + if (cmdBodyBuf == null) + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : Command not found. Check the first word in this line."; + return; + } + newContent.Body = cmdBodyBuf.Clone(); + newContent.Type = "command"; + splitlinelist.RemoveAt(0); + ParamsTypeCheck(splitlinelist, newContent); + } + + private void ParamsTypeCheck(List splitlinelist, CommandFileLine newContent) + { + NumberStyles style; + if (newContent.Body.Params.Count != 0) + { + if (newContent.Body.Params[newContent.Body.Params.Count - 1].Type.ToLower() != "raw") + { + if (splitlinelist.Count != newContent.Body.Params.Count) + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong number of parameters"; + return; + } + } + } + else{ + if (splitlinelist.Count != 0) + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong number of parameters"; + return; + } + } + for (int SL = 0; SL < newContent.Body.Params.Count; SL++) + { + if (splitlinelist[SL].Contains("0x")) + { + style = NumberStyles.HexNumber; + splitlinelist[SL] = splitlinelist[SL].Replace("0x", ""); //TryParseは0xがあると成功しない + } + else + { + style = NumberStyles.Integer; + } + switch (newContent.Body.Params[SL].Type.ToLower()) + { + //TODO: How to show the number of wrong-type parameters + case "int8_t": + case "int8": + SByte sbyte_val; + if (SByte.TryParse(splitlinelist[SL], style, CultureInfo.InvariantCulture, out sbyte_val)) + { + if(style == NumberStyles.HexNumber) + { + splitlinelist[SL] = "0x" + splitlinelist[SL]; + } + newContent.Body.Params[SL].Value = splitlinelist[SL]; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong type of parameters"; + } + break; + case "uint8_t": + case "uint8": + Byte byte_val; + if (Byte.TryParse(splitlinelist[SL], style, CultureInfo.InvariantCulture, out byte_val)) + { + if (style == NumberStyles.HexNumber) + { + splitlinelist[SL] = "0x" + splitlinelist[SL]; + } + newContent.Body.Params[SL].Value = splitlinelist[SL]; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong type of parameters"; + } + break; + case "int16_t": + case "int16": + Int16 int16_val; + if (Int16.TryParse(splitlinelist[SL], style, CultureInfo.InvariantCulture, out int16_val)) + { + if (style == NumberStyles.HexNumber) + { + splitlinelist[SL] = "0x" + splitlinelist[SL]; + } + newContent.Body.Params[SL].Value = splitlinelist[SL]; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong type of parameters"; + } + break; + case "uint16_t": + case "uint16": + UInt16 uint16_val; + if (UInt16.TryParse(splitlinelist[SL], style, CultureInfo.InvariantCulture, out uint16_val)) + { + if (style == NumberStyles.HexNumber) + { + splitlinelist[SL] = "0x" + splitlinelist[SL]; + } + newContent.Body.Params[SL].Value = splitlinelist[SL]; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong type of parameters"; + } + break; + case "int32_t": + case "int32": + Int32 int32_val; + if (Int32.TryParse(splitlinelist[SL], style, CultureInfo.InvariantCulture, out int32_val)) + { + if (style == NumberStyles.HexNumber) + { + splitlinelist[SL] = "0x" + splitlinelist[SL]; + } + newContent.Body.Params[SL].Value = splitlinelist[SL]; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong type of parameters"; + } + break; + case "uint32_t": + case "uint32": + UInt32 uint32_val; + if (UInt32.TryParse(splitlinelist[SL], style, CultureInfo.InvariantCulture, out uint32_val)) + { + if (style == NumberStyles.HexNumber) + { + splitlinelist[SL] = "0x" + splitlinelist[SL]; + } + newContent.Body.Params[SL].Value = splitlinelist[SL]; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong type of parameters"; + } + break; + case "float": + float float_val; + if (float.TryParse(splitlinelist[SL], out float_val)) + { + newContent.Body.Params[SL].Value = splitlinelist[SL]; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong type of parameters"; + } + break; + case "double": + double double_val; + if (double.TryParse(splitlinelist[SL], out double_val)) + { + newContent.Body.Params[SL].Value = splitlinelist[SL]; + } + else + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : wrong type of parameters"; + } + break; + case "raw": + if (style != NumberStyles.HexNumber) + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : The raw parameter should be HEX."; + break; + } + splitlinelist[SL] = "[0x" + splitlinelist[SL]; + newContent.Body.Params[SL].Value = splitlinelist[SL]; + for (int i = SL + 1; i < splitlinelist.Count; i++) + { + if (!splitlinelist[i].Contains("0x")) + { + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : The raw parameter should be HEX."; + break; + } + newContent.Body.Params[SL].Value += "/" + splitlinelist[i]; + } + newContent.Body.Params[SL].Value += "]"; + break; + default: + newContent.SyntaxError = true; + newContent.ErrorMessage = "\"Command\" : undefined type (check CMD_DB)"; + break; + } + } + } + } +} diff --git a/aspnetapp/WINGS/Data/Interfaces/ICommandFileLogRepository.cs b/aspnetapp/WINGS/Data/Interfaces/ICommandFileLogRepository.cs new file mode 100644 index 0000000..c37df47 --- /dev/null +++ b/aspnetapp/WINGS/Data/Interfaces/ICommandFileLogRepository.cs @@ -0,0 +1,15 @@ +using System.IO; +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Models; + +namespace WINGS.Data +{ + public interface ICommandFileLogRepository + { + Task AddHistoryAsync(string opid, CommandFileLineLog command_file_line_log, string commanderId); + void InitializeLogFiles(string opid); + Stream GetLogFileStream(string opid); + List GetCmdLogHistory(string opid); + } +} diff --git a/aspnetapp/WINGS/Data/Interfaces/ICommandFileRepository.cs b/aspnetapp/WINGS/Data/Interfaces/ICommandFileRepository.cs new file mode 100644 index 0000000..bca01bf --- /dev/null +++ b/aspnetapp/WINGS/Data/Interfaces/ICommandFileRepository.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Models; + +namespace WINGS.Data +{ + public interface ICommandFileRepository + { + Task> LoadCommandFileIndexesAsync(TlmCmdFileConfig config); + Task LoadCommandFileAsync(TlmCmdFileConfig config, CommandFileIndex index, List commandDb); + } +} diff --git a/aspnetapp/WINGS/Data/Interfaces/IDbRepository.cs b/aspnetapp/WINGS/Data/Interfaces/IDbRepository.cs new file mode 100644 index 0000000..8adbb81 --- /dev/null +++ b/aspnetapp/WINGS/Data/Interfaces/IDbRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Models; + +namespace WINGS.Data +{ + public interface IDbRepository + { + Task> LoadAllFilesAsync(TlmCmdFileConfig config); + } +} diff --git a/aspnetapp/WINGS/Data/Interfaces/ILayoutRepository.cs b/aspnetapp/WINGS/Data/Interfaces/ILayoutRepository.cs new file mode 100644 index 0000000..ebc0115 --- /dev/null +++ b/aspnetapp/WINGS/Data/Interfaces/ILayoutRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Models; + +namespace WINGS.Data +{ + public interface ILayoutRepository + { + Task> LoadAllFilesAsync(TlmCmdFileConfig config); + void SaveLayoutAsync(TlmCmdFileConfig config, string name, string lytStr); + void RenameLayoutAsync(TlmCmdFileConfig config, string name, string oldName); + void DeleteLayoutAsync(TlmCmdFileConfig config, string name); + } +} diff --git a/aspnetapp/WINGS/Data/Interfaces/ITelemetryLogRepository.cs b/aspnetapp/WINGS/Data/Interfaces/ITelemetryLogRepository.cs new file mode 100644 index 0000000..b453c5f --- /dev/null +++ b/aspnetapp/WINGS/Data/Interfaces/ITelemetryLogRepository.cs @@ -0,0 +1,18 @@ +using System.IO; +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Models; + +namespace WINGS.Data +{ + public interface ITelemetryLogRepository + { + Task AddHistoryAsync(string opid, TelemetryPacket packet); + List GetTelemetryHistory(string opid, List telemetryDb); + List GetPacketsWithData(string opid); + List GetRecordPacketsWithData(string opid); + void InitializeLogFiles(string opid, List telemetryDb); + Stream GetLogFileStream(string opid, string packetName); + Stream GetRecordLogFileStream(string opid, string packetName); + } +} diff --git a/aspnetapp/WINGS/Data/LayoutRepository.cs b/aspnetapp/WINGS/Data/LayoutRepository.cs new file mode 100644 index 0000000..9f0a367 --- /dev/null +++ b/aspnetapp/WINGS/Data/LayoutRepository.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using WINGS.Models; +using WINGS.Library; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace WINGS.Data +{ + /// + /// Provides methods for reading layout db files + /// + public class LayoutRepository : ILayoutRepository + { + private readonly IWebHostEnvironment _env; + + public LayoutRepository(IWebHostEnvironment env) + { + _env = env; + } + + /// + /// Reads the layout db JSON files defined in config as they are + /// + /// Config of the db files + /// Returns an enumerable collection of layout definitions that are read from layout db files + /// Undefined config of the file + public async Task> LoadAllFilesAsync(TlmCmdFileConfig config) + { + string dirPath = Path.Combine(_env.ContentRootPath, config.LayoutInfo.DirPath, "lyts"); + if (!Directory.Exists(dirPath)) + { + // Try to create the directory. + DirectoryInfo di = Directory.CreateDirectory(dirPath); + } + + + var filePaths = GetFilePathsAsync(config); + + // Parallel file loading + var sem = new SemaphoreSlim(10); // Maximum number of parallel excecution + + static string ReadAllLines(string filePath, string encodingName) + { + StreamReader sr = new StreamReader(filePath, Encoding.GetEncoding(encodingName)); + string allLine = sr.ReadToEnd(); + sr.Close(); + + return allLine; + } + + int count = 0; + var loadTasks = filePaths.Select(async filePath => + { + await sem.WaitAsync(); + try + { + string lytStr = ReadAllLines(filePath,"utf-8"); + var lyt = JsonSerializer.Deserialize(lytStr, new JsonSerializerOptions{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + lyt.id = count; + count = count + 1; + int ind = filePath.IndexOf("lyts")+5; + string jsonname = filePath.Substring(ind); + lyt.name = jsonname.Substring(0,jsonname.Length-5); + return lyt; + } + finally + { + sem.Release(); + } + }); + var layouts = await Task.WhenAll(loadTasks); + return layouts; + } + public void SaveLayoutAsync(TlmCmdFileConfig config, string name, string lytStr) + { + string dirPath = Path.Combine(_env.ContentRootPath, config.LayoutInfo.DirPath, "lyts"); + string fileName = dirPath + "/" + name + ".json"; + string encodingName = "utf-8"; + StreamWriter sw = new StreamWriter(fileName, true, Encoding.GetEncoding(encodingName)); + sw.Write(lytStr); + sw.Close(); + } + + public void RenameLayoutAsync(TlmCmdFileConfig config, string name, string oldName) + { + string dirPath = Path.Combine(_env.ContentRootPath, config.LayoutInfo.DirPath, "lyts"); + string oldFilePath = dirPath + "/" + oldName + ".json"; + string newFilePath = dirPath + "/" + name + ".json"; + File.Move(oldFilePath, newFilePath); + } + + public void DeleteLayoutAsync(TlmCmdFileConfig config, string name) + { + string dirPath = Path.Combine(_env.ContentRootPath, config.LayoutInfo.DirPath, "lyts"); + string filePath = dirPath + "/" + name + ".json"; + File.Delete(filePath); + } + private IEnumerable GetFilePathsAsync(TlmCmdFileConfig config) + { + string dirPath = Path.Combine(_env.ContentRootPath, config.LayoutInfo.DirPath, "lyts"); + return Directory.EnumerateFiles(dirPath, "*.json", SearchOption.TopDirectoryOnly); + } + } +} diff --git a/aspnetapp/WINGS/Data/TelemetryDbRepository.cs b/aspnetapp/WINGS/Data/TelemetryDbRepository.cs new file mode 100644 index 0000000..5fff4f5 --- /dev/null +++ b/aspnetapp/WINGS/Data/TelemetryDbRepository.cs @@ -0,0 +1,230 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using WINGS.Models; +using WINGS.Library; +using System.Text; + +namespace WINGS.Data +{ + /// + /// Provides methods for reading telemetry db files + /// + public class TelemetryDbRepository : IDbRepository + { + private readonly IWebHostEnvironment _env; + + public TelemetryDbRepository(IWebHostEnvironment env) + { + _env = env; + } + + /// + /// Reads the telemetry db files defined in config and parses to the TelemetryPacket objects + /// + /// Config of the db files + /// Returns an enumerable collection of telemetry definitions that are read from telemetry db files + /// Undefined config of the file + public async Task> LoadAllFilesAsync(TlmCmdFileConfig config) + { + var telemetryDb = new List(); + + // Parallel file loading + var sem = new SemaphoreSlim(10); // Maximum number of parallel excecution + + foreach (var c in config.TlmDBInfo) + { + var filePaths = await GetDbFilePathsAsync(config.Location, c); + var loadTasks = filePaths.Select(async filePath => + { + await sem.WaitAsync(); + try + { + return await LoadFileAsync(config.Location, c, filePath); + } + finally + { + sem.Release(); + } + }); + var packets = await Task.WhenAll(loadTasks); + + foreach (var packet in packets) + { + if (packet.PacketInfo != null) // Add only "ENABLE" + { + telemetryDb.Add(packet); + } + } + } + return telemetryDb; + } + + private async Task LoadFileAsync(TlmCmdFileLocation location, TlmCmdFileLocationInfo tlmDBInfo, string filePath) + { + string[] cols; + var reader = await GetDbFileReaderAsync(location, tlmDBInfo, filePath); + var telemetries = new List(); + + reader.ReadLine(); // APID + + // Packet Info + cols = reader.ReadLine().Split(","); + var packetId = cols[2]; + var packetName = Path.GetFileNameWithoutExtension(filePath); + if (packetName.IndexOf("_TLM_DB_") != -1) + { + packetName = packetName.Substring(packetName.IndexOf("_TLM_DB_") + 8); + } + var packetInfo = new PacketInfo(){ + Id = packetId, + Name = packetName, + IsRealtimeData = true, // add packet only for realtime tlm (record tlms are registered when SetTelemetryValuesAsync() in TmPacketAnalyzerBase.cs is called) + IsRestricted = false + }; + + // ENA/DIS + cols = reader.ReadLine().Split(","); + if (cols[2] == "DISABLE") + { + return new TelemetryPacket(); + } + + // IsRestricted + cols = reader.ReadLine().Split(","); + if (cols[2] == "TRUE") + { + packetInfo.IsRestricted = true; + } + + for (int i = 0; i < 4; i++) + { + reader.ReadLine(); + } + + using (var parser = new TextFieldParser(reader)) + { + parser.SetDelimiters(","); + parser.HasFieldsEnclosedInQuotes = true; + parser.TrimWhiteSpace = true; + + while (!parser.EndOfData) + { + cols = parser.ReadFields(); + if (cols.All( x => x == "")) { break; } + + var convType = cols[8].ToString(); + var poly = new double[]{0,0,0,0,0,0}; + var statusStr = ""; + var status = new Dictionary(); + + switch (convType) + { + case "POLY": + for (var i=0; i<6; i++) + { + poly[i] = cols[i+9] == "" ? 0 : Convert.ToDouble(cols[i+9]); + } + break; + + case "STATUS": + statusStr = cols[15].ToString(); + status = ParseStatus(statusStr); + break; + + default: + break; + } + + telemetries.Add(new Telemetry() + { + TelemetryInfo = new TelemetryInfo() + { + Name = packetName + "." + cols[1], + Type = cols[2], + Unit = cols[3], + OctetPos = Convert.ToInt32(cols[5]), + BitPos = Convert.ToInt32(cols[6]), + BitLen = Convert.ToInt32(cols[7]), + ConvType = convType, + Poly = poly, + Status = status, + Description = cols[16] + }, + TelemetryValue = new TelemetryValue() + }); + } + } + + return new TelemetryPacket(){ + PacketInfo = packetInfo, + Telemetries = telemetries + }; + } + + private Dictionary ParseStatus(string statusStr) + { + var status = new Dictionary(); + string[] defs; + if (statusStr == "") + { + return status; + } + else if (statusStr.Contains("@@")) + { + defs = statusStr.Split("@@"); + for (var i = 0; i < defs.Length; i++) + { + defs[i] = defs[i].Trim(); + } + } + else{ + defs = statusStr.Split(", "); + for (var i = 0; i < defs.Length; i++) + { + defs[i] = defs[i].Trim(); + } + } + foreach (var def in defs) + { + var vals = def.Split("="); + if (vals[0].Contains("0x")) + { + vals[0] = vals[0].Replace("0x", "").Trim(); + var valnum = Convert.ToUInt32(vals[0], 16); + vals[0] = Convert.ToString(valnum); + } + status.Add(vals[0].Trim(), vals[1].Trim()); + } + return status; + } + + private Task> GetDbFilePathsAsync(TlmCmdFileLocation location, TlmCmdFileLocationInfo tlmDBInfo) + { + switch (location) + { + case TlmCmdFileLocation.Local: + string dirPath = Path.Combine(_env.ContentRootPath, tlmDBInfo.DirPath, "tlmdb"); + return Task.FromResult(Directory.EnumerateFiles(dirPath, "*.csv", SearchOption.TopDirectoryOnly)); + default: + throw new ArgumentException("Undefined file location"); + } + } + + private Task GetDbFileReaderAsync(TlmCmdFileLocation location, TlmCmdFileLocationInfo tlmDBInfo, string filePath) + { + switch (location) + { + case TlmCmdFileLocation.Local: + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + return Task.FromResult(new StreamReader(filePath, Encoding.GetEncoding("shift-jis"))); + default: + throw new ArgumentException("Undefined file location"); + } + } + } +} diff --git a/aspnetapp/WINGS/Data/TelemetryLogRepository.cs b/aspnetapp/WINGS/Data/TelemetryLogRepository.cs new file mode 100644 index 0000000..0f6face --- /dev/null +++ b/aspnetapp/WINGS/Data/TelemetryLogRepository.cs @@ -0,0 +1,235 @@ +using System; +using System.Linq; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using WINGS.Models; + +namespace WINGS.Data +{ + /// + /// Provides methods for handling csv telemetry logs + /// + public class TelemetryLogRepository : ITelemetryLogRepository + { + private readonly IWebHostEnvironment _env; + + public TelemetryLogRepository(IWebHostEnvironment env) + { + _env = env; + } + + /// + /// Add the telemetry log to csv files per packet + /// + /// Operation id + /// Telemetry packet to save the logs + public async Task AddHistoryAsync(string opid, TelemetryPacket packet) + { + if (packet.PacketInfo.IsRealtimeData) + { // for realtime tlm + string filePath = Path.Combine(_env.ContentRootPath, "Logs", opid, "tlmlog", packet.PacketInfo.Name + ".csv"); + using (var sw = new StreamWriter(filePath, true)) + { + var sb = new StringBuilder(); + sb.Append(packet.Telemetries.First().TelemetryValue.Time + ","); + foreach (var tlm in packet.Telemetries) + { + sb.Append(tlm.TelemetryValue.Value + ","); + sb.Append(tlm.TelemetryValue.RawValue + ","); + } + sb.Remove(sb.Length - 1, 1); + sb.Append("\r\n"); + await sw.WriteAsync(sb.ToString()); + } + } + else + { // for recorded tlm + string filePath = Path.Combine(_env.ContentRootPath, "Logs", opid, "recordtlmlog", packet.PacketInfo.Name + ".csv"); + using (var sw = new StreamWriter(filePath, true)) + { + var sb = new StringBuilder(); + sb.Append(packet.Telemetries.First().TelemetryValue.Time + ","); + sb.Append(packet.Telemetries.First().TelemetryValue.TI + ","); + foreach (var tlm in packet.Telemetries) + { + sb.Append(tlm.TelemetryValue.Value + ","); + sb.Append(tlm.TelemetryValue.RawValue + ","); + } + sb.Remove(sb.Length - 1, 1); + sb.Append("\r\n"); + await sw.WriteAsync(sb.ToString()); + } + } + } + + public List GetTelemetryHistory(string opid, List telemetryDb) + { + string dirPath = Path.Combine(_env.ContentRootPath, "Logs", opid, "tlmlog"); + string[] fileNames = Directory.GetFiles(dirPath, "*.csv"); + var telemetryPacketHistories = new List(); + + foreach (var fileName in fileNames) + { + string[] cols; + FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + var sr = new StreamReader(fs); + var telemetryPacketHistory = new TelemetryPacketHistory() { + PacketInfo = new PacketInfo(), + TelemetryHistories = new List() + }; + + var packet = telemetryDb.Find(x => x.PacketInfo.Name == Path.GetFileNameWithoutExtension(fileName)); + telemetryPacketHistory.PacketInfo = packet.PacketInfo; + foreach (var tlm in packet.Telemetries) + { + telemetryPacketHistory.TelemetryHistories.Add(new TelemetryHistory() + { + TelemetryInfo = tlm.TelemetryInfo, + TelemetryValues = new List() + }); + } + + List tlmNames = new List(sr.ReadLine().Split(",")); // header + tlmNames.RemoveAt(0); // remove time + while (!sr.EndOfStream) + { + cols = sr.ReadLine().Split(","); + var time = cols[0]; + for (int i = 0; i < tlmNames.Count; i++) + { + var tlmName = (tlmNames[i].IndexOf('[') > 0)? tlmNames[i].Substring(0, tlmNames[i].IndexOf('[')) : tlmNames[i]; + if (i%2 == 0) // skip raw values + { + telemetryPacketHistory.TelemetryHistories.Find(x => x.TelemetryInfo.Name == tlmName).TelemetryValues.Add(new TelemetryValue() + { + Time = time, + Value = cols[i+1], + RawValue = cols[i+2], + }); + } + } + } + telemetryPacketHistories.Add(telemetryPacketHistory); + } + return telemetryPacketHistories; + } + + /// + /// Lists the names of packets for which telemetry data exists in the csv files + /// + /// Operation id + public List GetPacketsWithData(string opid) + { + var packetNames = new List(); + string dirPath = Path.Combine(_env.ContentRootPath, "Logs", opid, "tlmlog"); + string[] fileNames = Directory.GetFiles(dirPath, "*.csv"); + + foreach (var fileName in fileNames) + { + var sr = new StreamReader(fileName); + sr.ReadLine(); // header + if (!sr.EndOfStream) + { + string packetName = Path.GetFileNameWithoutExtension(fileName); + packetNames.Add(packetName); + } + } + return packetNames; + } + public List GetRecordPacketsWithData(string opid) + { + var packetNames = new List(); + string dirPath = Path.Combine(_env.ContentRootPath, "Logs", opid, "recordtlmlog"); + string[] fileNames = Directory.GetFiles(dirPath, "*.csv"); + + foreach (var fileName in fileNames) + { + var sr = new StreamReader(fileName); + sr.ReadLine(); // header + if (!sr.EndOfStream) + { + string packetName = Path.GetFileNameWithoutExtension(fileName); + packetNames.Add(packetName); + } + } + return packetNames; + } + + /// + /// Create new files and write headers to initialize log files + /// + /// Operation id + /// The definition of telemetry + public void InitializeLogFiles(string opid, List telemetryDb) + { + // for realtime tlm + string dirPath = Path.Combine(_env.ContentRootPath, "Logs", opid, "tlmlog"); + Directory.CreateDirectory(dirPath); + + foreach (var packet in telemetryDb) + { + var sb = new StringBuilder(); + sb.Append("Time,"); + foreach (var tlm in packet.Telemetries) + { + sb.Append(tlm.TelemetryInfo.Name + "[" + tlm.TelemetryInfo.Unit + "],"); + sb.Append(tlm.TelemetryInfo.Name + "[" + tlm.TelemetryInfo.Unit + "](RawData),"); + } + if (sb.Length > 0) + { + sb.Remove(sb.Length - 1, 1); + } + sb.Append("\r\n"); + string filePath = Path.Combine(dirPath, packet.PacketInfo.Name + ".csv"); + using (var sw = new StreamWriter(filePath)) + { + sw.Write(sb.ToString()); + } + } + + // for recorded tlm + dirPath = Path.Combine(_env.ContentRootPath, "Logs", opid, "recordtlmlog"); + Directory.CreateDirectory(dirPath); + foreach (var packet in telemetryDb) + { + var sb = new StringBuilder(); + sb.Append("Time,"); // Time when arrived at WINGS + sb.Append("TI,"); // TI when recorded onboard + foreach (var tlm in packet.Telemetries) + { + sb.Append(tlm.TelemetryInfo.Name + "[" + tlm.TelemetryInfo.Unit + "],"); + sb.Append(tlm.TelemetryInfo.Name + "[" + tlm.TelemetryInfo.Unit + "](RawData),"); + } + if (sb.Length > 0) + { + sb.Remove(sb.Length - 1, 1); + } + sb.Append("\r\n"); + string filePath = Path.Combine(dirPath, packet.PacketInfo.Name + ".csv"); + using (var sw = new StreamWriter(filePath)) + { + sw.Write(sb.ToString()); + } + } + } + + /// + /// Returns a stream of the specified csv file + /// + /// Operation id + /// Telemetry packet name + public Stream GetLogFileStream(string opid, string packetName) + { + string filePath = Path.Combine(_env.ContentRootPath, "Logs", opid, "tlmlog", packetName + ".csv"); + return File.OpenRead(filePath); + } + public Stream GetRecordLogFileStream(string opid, string packetName) + { + string filePath = Path.Combine(_env.ContentRootPath, "Logs", opid, "recordtlmlog", packetName + ".csv"); + return File.OpenRead(filePath); + } + } +} diff --git a/aspnetapp/WINGS/Library/CRC.cs b/aspnetapp/WINGS/Library/CRC.cs new file mode 100644 index 0000000..30e21ed --- /dev/null +++ b/aspnetapp/WINGS/Library/CRC.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; + +namespace WINGS.Library +{ + public static class CRC + { + private static readonly List crc16ccitt_right_table = new List() + { + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, + 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, + 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, + 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, + 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, + 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, + 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, + 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, + 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, + 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, + 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, + 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, + 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, + 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, + 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, + 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, + 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, + 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, + 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, + 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, + 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, + 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, + 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, + 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, + 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, + 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, + 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, + 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, + 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 + }; + + private static readonly List crc16ccitt_left_table = new List() + { + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 + }; + + private static readonly List crc16ibm_right_table = new List() + { + 0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, + 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, + 0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, + 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, + 0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, + 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, + 0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, + 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, + 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240, + 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, + 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, + 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, + 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, + 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, + 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640, + 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, + 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240, + 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, + 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, + 0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, + 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, + 0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, + 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, + 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, + 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, + 0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440, + 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, + 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, + 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, + 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, + 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, + 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040 + }; + + private static readonly List crc16ibm_left_table = new List() + { + 0x0000, 0x8005, 0x800f, 0x000a, 0x801b, 0x001e, 0x0014, 0x8011, + 0x8033, 0x0036, 0x003c, 0x8039, 0x0028, 0x802d, 0x8027, 0x0022, + 0x8063, 0x0066, 0x006c, 0x8069, 0x0078, 0x807d, 0x8077, 0x0072, + 0x0050, 0x8055, 0x805f, 0x005a, 0x804b, 0x004e, 0x0044, 0x8041, + 0x00f0, 0x80f5, 0x80ff, 0x00fa, 0x80eb, 0x00ee, 0x00e4, 0x80e1, + 0x00a0, 0x80a5, 0x80af, 0x00aa, 0x80bb, 0x00be, 0x00b4, 0x80b1, + 0x8093, 0x0096, 0x009c, 0x8099, 0x0088, 0x808d, 0x8087, 0x0082, + 0x8183, 0x0186, 0x018c, 0x8189, 0x0198, 0x819d, 0x8197, 0x0192, + 0x01b0, 0x81b5, 0x81bf, 0x01ba, 0x81ab, 0x01ae, 0x01a4, 0x81a1, + 0x01e0, 0x81e5, 0x81ef, 0x01ea, 0x81fb, 0x01fe, 0x01f4, 0x81f1, + 0x81d3, 0x01d6, 0x01dc, 0x81d9, 0x01c8, 0x81cd, 0x81c7, 0x01c2, + 0x0140, 0x8145, 0x814f, 0x014a, 0x815b, 0x015e, 0x0154, 0x8151, + 0x8173, 0x0176, 0x017c, 0x8179, 0x0168, 0x816d, 0x8167, 0x0162, + 0x8123, 0x0126, 0x012c, 0x8129, 0x0138, 0x813d, 0x8137, 0x0132, + 0x0110, 0x8115, 0x811f, 0x011a, 0x810b, 0x010e, 0x0104, 0x8101, + 0x8303, 0x0306, 0x030c, 0x8309, 0x0318, 0x831d, 0x8317, 0x0312, + 0x0330, 0x8335, 0x833f, 0x033a, 0x832b, 0x032e, 0x0324, 0x8321, + 0x0360, 0x8365, 0x836f, 0x036a, 0x837b, 0x037e, 0x0374, 0x8371, + 0x8353, 0x0356, 0x035c, 0x8359, 0x0348, 0x834d, 0x8347, 0x0342, + 0x03c0, 0x83c5, 0x83cf, 0x03ca, 0x83db, 0x03de, 0x03d4, 0x83d1, + 0x83f3, 0x03f6, 0x03fc, 0x83f9, 0x03e8, 0x83ed, 0x83e7, 0x03e2, + 0x83a3, 0x03a6, 0x03ac, 0x83a9, 0x03b8, 0x83bd, 0x83b7, 0x03b2, + 0x0390, 0x8395, 0x839f, 0x039a, 0x838b, 0x038e, 0x0384, 0x8381, + 0x0280, 0x8285, 0x828f, 0x028a, 0x829b, 0x029e, 0x0294, 0x8291, + 0x82b3, 0x02b6, 0x02bc, 0x82b9, 0x02a8, 0x82ad, 0x82a7, 0x02a2, + 0x82e3, 0x02e6, 0x02ec, 0x82e9, 0x02f8, 0x82fd, 0x82f7, 0x02f2, + 0x02d0, 0x82d5, 0x82df, 0x02da, 0x82cb, 0x02ce, 0x02c4, 0x82c1, + 0x8243, 0x0246, 0x024c, 0x8249, 0x0258, 0x825d, 0x8257, 0x0252, + 0x0270, 0x8275, 0x827f, 0x027a, 0x826b, 0x026e, 0x0264, 0x8261, + 0x0220, 0x8225, 0x822f, 0x022a, 0x823b, 0x023e, 0x0234, 0x8231, + 0x8213, 0x0216, 0x021c, 0x8219, 0x0208, 0x820d, 0x8207, 0x0202 + }; + public static UInt16 CRC16CCITTRightCalc(byte[] input, UInt16 seed) + { + UInt16 ret = seed; + int length = input.Length; + for (int i = 0; i < length; i++) + { + ret = (UInt16)((ret >> 8) ^ crc16ccitt_right_table[(ret ^ (input[i])) & 0xff]); + } + return ret; + } + public static UInt16 CRC16CCITTLeftCalc(byte[] input, UInt16 seed) + { + UInt16 ret = seed; + int length = input.Length; + for (int i = 0; i < length; i++) + { + ret = (UInt16)((ret << 8) ^ crc16ccitt_left_table[((ret >> 8) ^ (input[i])) & 0xff]); + } + return ret; + } + public static UInt16 CRC16IBMRightCalc(byte[] input, UInt16 seed) + { + UInt16 ret = seed; + int length = input.Length; + for (int i = 0; i < length; i++) + { + ret = (UInt16)((ret >> 8) ^ crc16ibm_right_table[(ret ^ (input[i])) & 0xff]); + } + return ret; + } + public static UInt16 CRC16IBMLeftCalc(byte[] input, UInt16 seed) + { + UInt16 ret = seed; + int length = input.Length; + for (int i = 0; i < length; i++) + { + ret = (UInt16)((ret << 8) ^ crc16ibm_left_table[((ret >> 8) ^ (input[i])) & 0xff]); + } + return ret; + } + } +} diff --git a/aspnetapp/WINGS/Library/Paginator.cs b/aspnetapp/WINGS/Library/Paginator.cs new file mode 100644 index 0000000..0bf311c --- /dev/null +++ b/aspnetapp/WINGS/Library/Paginator.cs @@ -0,0 +1,39 @@ +using System; +using System.Text; +using System.Collections.Generic; +using WINGS.Models; + +namespace WINGS.Library +{ + public static class Paginator + { + public static PageMeta GetPageMeta(string baseUrl, int page, int size, int totalCount, Dictionary query) + { + var pageCount = (int)Math.Ceiling((double)totalCount/size); + if (page < 1) page = 1; + if (page > pageCount) page = pageCount; + + var sb = new StringBuilder(); + sb.Append("?page={0}&size={1}"); + foreach (KeyValuePair item in query) + { + sb.Append($"&{item.Key}={item.Value}"); + } + var queryStringBase = sb.ToString(); + + return new PageMeta + { + Page = page, + Size = size, + PageCount = pageCount, + Links = new PageLink{ + Self = baseUrl + string.Format(queryStringBase, page, size), + First = baseUrl + string.Format(queryStringBase, 1, size), + Previous = baseUrl + string.Format(queryStringBase, page > 1 ? page - 1 : 1, size), + Next = baseUrl + string.Format(queryStringBase, page < pageCount - 1 ? page + 1 : pageCount, size), + Last = baseUrl + string.Format(queryStringBase, pageCount, size) + } + }; + } + } +} diff --git a/aspnetapp/WINGS/Library/TextFieldParser.cs b/aspnetapp/WINGS/Library/TextFieldParser.cs new file mode 100644 index 0000000..000ae82 --- /dev/null +++ b/aspnetapp/WINGS/Library/TextFieldParser.cs @@ -0,0 +1,520 @@ +// +// TextFieldParserForDotNetCore +// +// https://github.com/Taka414/TextFieldParserForDotNetCore +// +// This supports for reading a CSV file with commas in a column +// Example: +// 1,2,3,4,5 -> [1], [2], [3], [4], [5] +// "a,a",2,3,4,"5" -> [a,a], [2], [3], [4], [5] +// "a,a", 2, 3,"4", 5 -> [a,a] [2] [3] ["4"] [5] +// + +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace WINGS.Library +{ + [Serializable] + public class MalformedLineException : Exception + { + public MalformedLineException() { } + public MalformedLineException(string message, long line) : base(message) { } + public MalformedLineException(string message, Exception inner) : base(message, inner) { } + protected MalformedLineException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } + + /// + /// Indicates whether text fields are delimited or fixed width. + /// [Jp]テキスト フィールドが区切り形式か固定幅形式かを示します。 + /// + public enum FieldType + { + /// + /// Indicates that the fields are delimited. + /// [JP] フィールドが区切り形式であることを示します。 + /// + + Delimited = 0, + /// + /// Indicates that the fields are fixed width. + /// [JP] フィールドが固定幅形式であることを示します。 + /// + FixedWidth = 1 + } + + /// + /// Provides methods and properties for parsing structured text files. + /// [JP] 構造化テキスト ファイルの解析に使用するメソッドとプロパティを提供します。 + /// + public class TextFieldParser : IDisposable + { + // Node:: + // + // [Sample,csv] + // 1,2,3,4,5 // read OK + // "a,a",2,3,4,"5" // read OK + // "a,a", 2, 3, "4" , 5 // can not read pattern [ "4" ]. with no error ended. + + // + // Fields + // - - - - - - - - - - - - - - - - - - - - + #region Fields + + private TextReader reader; + private bool leaveOpen = false; + private int[] fieldWidths = null; + private Queue peekedLine = new Queue(); + private int minFieldLength; + + #endregion + + // + // Properties + // - - - - - - - - - - - - - - - - - - - - + #region Properties + + /// + /// Defines comment tokens. A comment token is a string that, when placed at the beginning of a line, indicates that the line is a comment and should be ignored by the parser. + /// [Jp] コメント トークンを定義します。 コメント トークンとは、コメント行であることを示すために、行頭に配置される文字列です。コメント トークンの配置された行は、パーサーによって無視されます。 + /// + public string[] CommentTokens { get; set; } = new string[] { }; + + /// + /// Defines the delimiters for a text file. + /// [JP] テキスト ファイルの区切り記号を定義します。 + /// + public string[] Delimiters { get; set; } + + /// + /// Returns true if there are no non-blank, non-comment lines between the current cursor position and the end of the file. + /// [JP] 現在のカーソル位置とファイルの終端との間に、空行またはコメント行以外のデータが存在しない場合、true を返します。 + /// + public bool EndOfData => this.PeekChars(1) == null; + + /// + /// Returns the line that caused the most recent exception. + /// [Jp] 直前に発生した MalformedLineException 例外の原因となった行を返します。 + /// + public string ErrorLine { get; private set; } = string.Empty; + + /// + /// Returns the number of the line that caused the most recent exception. + /// [Jp] 直前の MalformedLineException 例外が発生した行の番号を返します。 + /// + public long ErrorLineNumber { get; private set; } = -1; + + /// + /// Denotes the width of each column in the text file being parsed. + /// [Jp] 解析するテキスト ファイルの各列の幅を表します。 + /// + public int[] FieldWidths + { + get => this.fieldWidths; + set + { + this.fieldWidths = value; + if (this.fieldWidths != null) + { + this.minFieldLength = 0; + for (int i = 0; i <= this.fieldWidths.Length - 1; i++) + { + this.minFieldLength += value[i]; + } + } + } + } + + /// + /// Denotes whether fields are enclosed in quotation marks when a delimited file is being parsed. + /// [Jp] 区切り記号入りファイルを解析する場合に、フィールドが引用符で囲まれているかどうかを示します。 + /// + public bool HasFieldsEnclosedInQuotes { get; set; } = true; + + /// + /// Returns the current line number, or returns -1 if no more characters are available in the stream. + /// [Jp] 現在の行番号を返します。ストリームから取り出す文字がなくなった場合は -1 を返します。 + /// + public long LineNumber { get; private set; } = -1; + + /// + /// Indicates whether the file to be parsed is delimited or fixed-width. + /// [Jp] 解析対象のファイルが区切り形式か固定幅形式かを示します。 + /// + public FieldType TextFieldType { get; set; } = FieldType.Delimited; + + /// + /// Indicates whether leading and trailing white space should be trimmed from field values. + /// [Jp] フィールド値から前後の空白をトリムするかどうかを示します。 + /// + public bool TrimWhiteSpace { get; set; } = true; + + #endregion + + // + // Constructors + // - - - - - - - - - - - - - - - - - - - - + #region Constructors + + public TextFieldParser(Stream stream) => this.reader = new StreamReader(stream); + public TextFieldParser(TextReader reader) => this.reader = reader; + public TextFieldParser(string path) => this.reader = new StreamReader(path); + public TextFieldParser(Stream stream, Encoding defaultEncoding) => this.reader = new StreamReader(stream, defaultEncoding); + public TextFieldParser(string path, Encoding defaultEncoding) => this.reader = new StreamReader(path, defaultEncoding); + public TextFieldParser(Stream stream, Encoding defaultEncoding, bool detectEncoding) => this.reader = new StreamReader(stream, defaultEncoding, detectEncoding); + public TextFieldParser(string path, Encoding defaultEncoding, bool detectEncoding) => this.reader = new StreamReader(path, defaultEncoding, detectEncoding); + public TextFieldParser(Stream stream, Encoding defaultEncoding, bool detectEncoding, bool leaveOpen) + { + this.reader = new StreamReader(stream, defaultEncoding, detectEncoding); + this.leaveOpen = leaveOpen; + } + + ~TextFieldParser() + { + this.Dispose(false); + } + + #endregion + + // + // Public Functions + // - - - - - - - - - - - - - - - - - - - - + #region Public Functions + + /// + /// Closes the current object. + /// [Jp] 現在の オブジェクトを閉じます。 + /// + public void Close() + { + if (this.reader != null && this.leaveOpen == false) + { + this.reader.Close(); + } + this.reader = null; + } + + /// + /// + /// [Jp] カーソルを進めずに、指定された文字数を読み込みます。 + /// + public string PeekChars(int numberOfChars) + { + if (numberOfChars < 1) + { + throw (new ArgumentException("numberOfChars has to be a positive, non-zero number", "numberOfChars")); + } + string[] peekedLines; + string theLine = null; + if (this.peekedLine.Count > 0) + { + peekedLines = this.peekedLine.ToArray(); + for (int i = 0; i <= this.peekedLine.Count - 1; i++) + { + if (this.IsCommentLine(Line: peekedLines[i]) == false) + { + theLine = peekedLines[i]; + break; + } + } + } + if (theLine == null) + { + do + { + theLine = this.reader.ReadLine(); + this.LineNumber++; + this.peekedLine.Enqueue(theLine); + + } while (!(theLine == null || this.IsCommentLine(theLine) == false)); + } + if (theLine != null) + { + if (theLine.Length <= numberOfChars) + { + return theLine; + } + else + { + return theLine.Substring(0, numberOfChars); + } + } + + else + { + return null; + } + } + + /// + /// Reads the specified number of characters without advancing the cursor. + /// [Jp] 現在行のすべてのフィールドを読み込んで文字列の配列として返し、次のデータが格納されている行にカーソルを進めます。 + /// + public string[] ReadFields() + { + switch (this.TextFieldType) + { + case FieldType.Delimited: + return this.GetDelimitedFields(); + + case FieldType.FixedWidth: + default: + throw new NotSupportedException("Sorry. this type is not suported."); + + // case FieldType.FixedWidth: + // return GetWidthFields(); + // default: + // return GetDelimitedFields(); + } + } + + /// + /// Returns the current line as a string and advances the cursor to the next line. + /// [Jp] 現在の行を文字列として返し、カーソルを次の行に進めます。 + /// + public string ReadLine() + { + if (this.peekedLine.Count > 0) + { + return this.peekedLine.Dequeue(); + } + if (this.LineNumber == -1) + { + this.LineNumber = 1; + } + else + { + this.LineNumber++; + } + return this.reader.ReadLine(); + } + + /// + /// Reads the remainder of the text file and returns it as a string. + /// [Jp] テキスト ファイルの残りの部分を読み込み、文字列として返します。 + /// + public string ReadToEnd() => this.reader.ReadToEnd(); + + /// + /// Sets the delimiters for the reader to the specified values, and sets the field type to . + /// [Jp] リーダーの区切り記号を指定された値に設定し、フィールドの種類を に設定します。 + /// + public void SetDelimiters(params string[] delimiters) => this.Delimiters = delimiters; + + /// + /// Indicates whether leading and trailing white space should be trimmed from field values. + /// [Jp] リーダーの区切り記号を指定の値に設定します。 + /// + public void SetFieldWidths(params int[] fieldWidths) => this.FieldWidths = fieldWidths; + + #endregion + + // + // IDisposable Support + // - - - - - - - - - - - - - - - - - - - - + #region IDisposable Support + + private bool disposedValue = false; // To detect redundant calls + // IDisposable + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + this.Close(); + } + this.disposedValue = true; + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + // + // Private Functions + // - - - - - - - - - - - - - - - - - - - - + #region Private Functions + + private string[] GetDelimitedFields() + { + if (this.Delimiters == null || this.Delimiters.Length == 0) + { + throw (new InvalidOperationException("Unable to read delimited fields because Delimiters is Nothing or empty.")); + } + List result = new List(); + string line; + int currentIndex = 0; + int nextIndex = 0; + line = this.GetNextLine(); + if (line == null) + { + return null; + } + while (!(nextIndex >= line.Length)) + { + string parts = this.GetNextField(line, currentIndex, ref nextIndex); + if (this.TrimWhiteSpace) + { + parts = parts.Trim(); // simple trim. white space only. + } + result.Add(parts); + currentIndex = nextIndex; + } + return result.ToArray(); + } + + private string GetNextField(string line, int startIndex, ref int nextIndex) + { + bool inQuote = false; + int currentindex = 0; + if (nextIndex == int.MinValue) + { + nextIndex = int.MaxValue; + return string.Empty; + } + + currentindex = startIndex; + + if (this.HasFieldsEnclosedInQuotes && line[currentindex] == '\"') + { + inQuote = true; + startIndex++; + } + + + bool mustMatch = false; + for (int j = startIndex; j <= line.Length - 1; j++) + { + if (inQuote) + { + if (line[j] == '\"') + { + inQuote = false; + mustMatch = true; + } + continue; + } + for (int i = 0; i <= this.Delimiters.Length - 1; i++) + { + if (string.Compare(line, j, this.Delimiters[i], 0, this.Delimiters[i].Length) == 0) + { + nextIndex = j + this.Delimiters[i].Length; + if (nextIndex == line.Length) + { + nextIndex = int.MinValue; + } + if (mustMatch) + { + return line.Substring(startIndex, j - startIndex - 1); + } + else + { + return line.Substring(startIndex, j - startIndex); + } + } + } + if (mustMatch) + { + this.RaiseDelimiterEx(line); + } + } + + if (inQuote) + { + this.RaiseDelimiterEx(line); + } + + nextIndex = line.Length; + if (mustMatch) + { + return line.Substring(startIndex, nextIndex - startIndex - 1); + } + else + { + return line.Substring(startIndex); + } + } + + private void RaiseDelimiterEx(string Line) + { + this.ErrorLineNumber = this.LineNumber; + this.ErrorLine = Line; + throw new MalformedLineException("Line " + this.ErrorLineNumber.ToString() + " cannot be parsed using the current Delimiters.", this.ErrorLineNumber); + } + + private void RaiseFieldWidthEx(string Line) + { + this.ErrorLineNumber = this.LineNumber; + this.ErrorLine = Line; + throw new MalformedLineException("Line " + this.ErrorLineNumber.ToString() + " cannot be parsed using the current FieldWidths.", this.ErrorLineNumber); + } + + private string[] GetWidthFields() + { + if (this.fieldWidths == null || this.fieldWidths.Length == 0) + { + throw (new InvalidOperationException("Unable to read fixed width fields because FieldWidths is Nothing or empty.")); + } + string[] result = new string[this.fieldWidths.Length - 1 + 1]; + int currentIndex = 0; + string line; + line = this.GetNextLine(); + if (line.Length < this.minFieldLength) + { + this.RaiseFieldWidthEx(line); + } + for (int i = 0; i <= result.Length - 1; i++) + { + if (this.TrimWhiteSpace) + { + result[i] = line.Substring(currentIndex, this.fieldWidths[i]).Trim(); + } + else + { + result[i] = line.Substring(currentIndex, this.fieldWidths[i]); + } + currentIndex += this.fieldWidths[i]; + } + return result; + } + + private bool IsCommentLine(string Line) + { + if (this.CommentTokens == null) + { + return false; + } + foreach (string str in this.CommentTokens) + { + if (Line.StartsWith(str)) + { + return true; + } + } + return false; + } + + private string GetNextRealLine() + { + string nextLine; + do + { + nextLine = this.ReadLine(); + } while (!(nextLine == null || this.IsCommentLine(nextLine) == false)); + return nextLine; + } + + private string GetNextLine() + { + return this.peekedLine.Count > 0 ? this.peekedLine.Dequeue() : this.GetNextRealLine(); + } + + #endregion + } +} diff --git a/aspnetapp/WINGS/Library/Zipper.cs b/aspnetapp/WINGS/Library/Zipper.cs new file mode 100644 index 0000000..d32ea23 --- /dev/null +++ b/aspnetapp/WINGS/Library/Zipper.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.IO.Compression; +using System.Collections.Generic; +using WINGS.Models; + +namespace WINGS.Library +{ + public static class Zipper + { + public static Stream GetZipStream(List zipItems) + { + var zipStream = new MemoryStream(); + using (var zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true)) + { + foreach (var zipItem in zipItems) + { + var entry = zip.CreateEntry(zipItem.Name); + using (var entryStream = entry.Open()) + { + zipItem.Content.CopyTo(entryStream); + } + } + } + zipStream.Position = 0; + return zipStream; + } + } +} diff --git a/aspnetapp/WINGS/Logs/.gitignore b/aspnetapp/WINGS/Logs/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/aspnetapp/WINGS/Logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/aspnetapp/WINGS/Migrations/ApplicationDbContextModelSnapshot.cs b/aspnetapp/WINGS/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..1306955 --- /dev/null +++ b/aspnetapp/WINGS/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,151 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WINGS.Data; + +#nullable disable + +namespace WINGS.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("WINGS.Models.CommandLog", b => + { + b.Property("OperationId") + .HasColumnType("varchar(255)"); + + b.Property("SentAt") + .HasColumnType("datetime(1)"); + + b.Property("CmdName") + .HasColumnType("varchar(255)"); + + b.Property("ExecTime") + .HasColumnType("int unsigned"); + + b.Property("ExecType") + .IsRequired() + .HasColumnType("varchar(64)"); + + b.Property("Param1") + .HasColumnType("varchar(64)"); + + b.Property("Param2") + .HasColumnType("varchar(64)"); + + b.Property("Param3") + .HasColumnType("varchar(64)"); + + b.Property("Param4") + .HasColumnType("varchar(64)"); + + b.Property("Param5") + .HasColumnType("varchar(64)"); + + b.Property("Param6") + .HasColumnType("varchar(64)"); + + b.HasKey("OperationId", "SentAt"); + + b.ToTable("CommandLogs"); + }); + + modelBuilder.Entity("WINGS.Models.Component", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("LocalDirPath") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)"); + + b.Property("TcPacketKey") + .IsRequired() + .HasColumnType("varchar(64)"); + + b.Property("TmPacketKey") + .IsRequired() + .HasColumnType("varchar(64)"); + + b.HasKey("Id"); + + b.ToTable("Components"); + }); + + modelBuilder.Entity("WINGS.Models.Operation", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Comment") + .HasColumnType("longtext"); + + b.Property("ComponentId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(0)"); + + b.Property("FileLocation") + .IsRequired() + .HasColumnType("varchar(64)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("IsTmtcConnected") + .HasColumnType("tinyint(1)"); + + b.Property("PathNumber") + .HasColumnType("varchar(64)"); + + b.Property("TmtcTarget") + .IsRequired() + .HasColumnType("varchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("ComponentId"); + + b.ToTable("Operations"); + }); + + modelBuilder.Entity("WINGS.Models.CommandLog", b => + { + b.HasOne("WINGS.Models.Operation", "Operation") + .WithMany() + .HasForeignKey("OperationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Operation"); + }); + + modelBuilder.Entity("WINGS.Models.Operation", b => + { + b.HasOne("WINGS.Models.Component", "Component") + .WithMany() + .HasForeignKey("ComponentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Component"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/aspnetapp/WINGS/Models/Command.cs b/aspnetapp/WINGS/Models/Command.cs new file mode 100644 index 0000000..2def7a7 --- /dev/null +++ b/aspnetapp/WINGS/Models/Command.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; + +namespace WINGS.Models +{ + public enum CmdExecType + { + RT, + TL, + BL, + UTL + } + + public class Command + { + public string Component { get; set; } + public CmdExecType ExecType { get; set; } + public uint ExecTime { get; set; } + public string Name { get; set; } + public string Code { get; set; } + public string Target { get; set; } + public List Params { get; set; } + public bool IsDanger { get; set; } + public bool IsViaMobc { get; set; } + public bool IsRestricted { get; set; } + public string Description { get; set; } + + public Command Clone() + { + Command cloned = (Command)MemberwiseClone(); + if (this.Params != null) + { + cloned.Params = new List(); + foreach (var param in this.Params) + { + cloned.Params.Add((CommandParam)param.Clone()); + } + } + return cloned; + } + } + public class CommandParam + { + public string Name { get; set; } + public string Type { get; set; } + public string Value { get; set; } + public string Unit { get; set; } + public string Description { get; set; } + + public CommandParam Clone() + { + return (CommandParam)MemberwiseClone(); + } + } + + public class CommandFileLineLogs + { + public string Time { get; set; } + public string Commander { get; set; } + public string Content { get; set; } + public string Status { get; set; } + } +} diff --git a/aspnetapp/WINGS/Models/CommandFile.cs b/aspnetapp/WINGS/Models/CommandFile.cs new file mode 100644 index 0000000..8473c46 --- /dev/null +++ b/aspnetapp/WINGS/Models/CommandFile.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace WINGS.Models +{ + public class CommandFile + { + public CommandFileIndex Index { get; set; } + public List Content { get; set; } + } + + public class CommandFileIndex + { + public int FileId { get; set; } + public string Name { get; set; } + public string FilePath { get; set; } + public int CmdFileInfoIndex { get; set; } + } + + public class CommandFileLine + { + public string Type { get; set; } + public string Method { get; set; } + public dynamic Body { get; set; } + public string InlineComment { get; set; } + public bool StopFlag { get; set; } + public bool SyntaxError { get; set; } + public string ErrorMessage { get; set; } + } + + public class CommandFileLineLog + { + public CommandFileLineStatus Status { get; set; } + public CommandFileLine Request { get; set; } + } + + public class CommandFileLineStatus + { + public bool Success { get; set; } + public bool Error { get; set; } + } +} diff --git a/aspnetapp/WINGS/Models/CommandLog.cs b/aspnetapp/WINGS/Models/CommandLog.cs new file mode 100644 index 0000000..a120027 --- /dev/null +++ b/aspnetapp/WINGS/Models/CommandLog.cs @@ -0,0 +1,40 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace WINGS.Models +{ + public class CommandLog + { + [Column(TypeName = "datetime(1)")] + public DateTime SentAt { get; set; } + + [Column(TypeName = "varchar(64)")] + public CmdExecType ExecType { get; set; } + + public uint ExecTime { get; set; } + + [Column(TypeName = "varchar(255)")] + public string CmdName { get; set; } + + [Column(TypeName = "varchar(64)")] + public string Param1 { get; set; } + + [Column(TypeName = "varchar(64)")] + public string Param2 { get; set; } + + [Column(TypeName = "varchar(64)")] + public string Param3 { get; set; } + + [Column(TypeName = "varchar(64)")] + public string Param4 { get; set; } + + [Column(TypeName = "varchar(64)")] + public string Param5 { get; set; } + + [Column(TypeName = "varchar(64)")] + public string Param6 { get; set; } + + public string OperationId { get; set; } + public Operation Operation { get; set; } + } +} diff --git a/aspnetapp/WINGS/Models/Component.cs b/aspnetapp/WINGS/Models/Component.cs new file mode 100644 index 0000000..a022955 --- /dev/null +++ b/aspnetapp/WINGS/Models/Component.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace WINGS.Models +{ + public class Component + { + [Column(TypeName = "varchar(255)")] + public string Id { get; set; } + + [Required] + [Column(TypeName = "varchar(64)")] + public string Name { get; set; } + + [Required] + [Column(TypeName = "varchar(64)")] + public string TcPacketKey { get; set; } + + [Required] + [Column(TypeName = "varchar(64)")] + public string TmPacketKey { get; set; } + + [Column(TypeName = "longtext")] + public string LocalDirPath { get; set; } + } +} diff --git a/aspnetapp/WINGS/Models/Exception.cs b/aspnetapp/WINGS/Models/Exception.cs new file mode 100644 index 0000000..6677fa3 --- /dev/null +++ b/aspnetapp/WINGS/Models/Exception.cs @@ -0,0 +1,94 @@ +using System; + +namespace WINGS.Models +{ + public class ResourceReadException : Exception + { + public ResourceReadException() + { + } + public ResourceReadException(string message) + : base(message) + { + } + public ResourceReadException(string message, Exception inner) + : base(message, inner) + { + } + } + + public class ResourceCreateException : Exception + { + public ResourceCreateException() + { + } + public ResourceCreateException(string message) + : base(message) + { + } + public ResourceCreateException(string message, Exception inner) + : base(message, inner) + { + } + } + + public class ResourceUpdateException : Exception + { + public ResourceUpdateException() + { + } + public ResourceUpdateException(string message) + : base(message) + { + } + public ResourceUpdateException(string message, Exception inner) + : base(message, inner) + { + } + } + + public class ResourceDeleteException : Exception + { + public ResourceDeleteException() + { + } + public ResourceDeleteException(string message) + : base(message) + { + } + public ResourceDeleteException(string message, Exception inner) + : base(message, inner) + { + } + } + + public class ResourceNotFoundException : Exception + { + public ResourceNotFoundException() + { + } + public ResourceNotFoundException(string message) + : base(message) + { + } + public ResourceNotFoundException(string message, Exception inner) + : base(message, inner) + { + } + } + + public class IllegalContextException : Exception + { + public IllegalContextException() + { + } + public IllegalContextException(string message) + : base(message) + { + } + public IllegalContextException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/aspnetapp/WINGS/Models/Layout.cs b/aspnetapp/WINGS/Models/Layout.cs new file mode 100644 index 0000000..a90b4b7 --- /dev/null +++ b/aspnetapp/WINGS/Models/Layout.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; + +namespace WINGS.Models +{ + public class Layout + { + public TelemetryView telemetryView { get; set; } + public int id { get; set; } + public string name { get; set; } + } + + public class TelemetryView + { + public List allIndexes { get; set; } + public List blocks { get; set; } + public object contents { get; set; } + } + + public class TelemetryViewIndex + { + public string Id { get; set; } + public string Name { get; set; } + public string FilePath { get; set; } + public string Type { get; set; } + public List SelectedTelemetries { get; set; } + public string DataType {get; set; } + public string DataLength {get; set; } + public string YlabelMin {get; set; } + public string YlabelMax {get; set; } + } + + public class ViewBlockInfo + { + public List tabs { get; set; } + public int Activetab { get; set; } + } +} diff --git a/aspnetapp/WINGS/Models/Operation.cs b/aspnetapp/WINGS/Models/Operation.cs new file mode 100644 index 0000000..dd1576c --- /dev/null +++ b/aspnetapp/WINGS/Models/Operation.cs @@ -0,0 +1,41 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace WINGS.Models +{ + public enum TmtcTarget + { + TmtcIf, + Infostellar + } + + public class Operation + { + [Column(TypeName = "varchar(255)")] + public string Id { get; set; } + + [Column(TypeName = "varchar(64)")] + public string PathNumber { get; set; } + + [Column(TypeName = "longtext")] + public string Comment { get; set; } + + [Column(TypeName = "datetime(0)")] + public DateTime CreatedAt { get; set; } + + public bool IsRunning { get; set; } + + public bool IsTmtcConnected { get; set; } + + [Column(TypeName = "varchar(64)")] + public TlmCmdFileLocation FileLocation { get; set; } + + [Column(TypeName = "varchar(64)")] + public TmtcTarget TmtcTarget { get; set; } + + [Required] + public string ComponentId { get; set; } + public Component Component { get; set; } + } +} diff --git a/aspnetapp/WINGS/Models/Pagination.cs b/aspnetapp/WINGS/Models/Pagination.cs new file mode 100644 index 0000000..f544b47 --- /dev/null +++ b/aspnetapp/WINGS/Models/Pagination.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace WINGS.Models +{ + public class Pagination + { + public PageMeta Meta { get; set; } + public List Data { get; set; } + } + + public class PageMeta + { + public int Page { get; set; } + public int Size { get; set; } + public int PageCount { get; set; } + public PageLink Links { get; set; } + } + + public class PageLink + { + public string Self { get; set; } + public string First { get; set; } + public string Previous { get; set; } + public string Next { get; set; } + public string Last { get; set; } + } +} diff --git a/aspnetapp/WINGS/Models/Telemetry.cs b/aspnetapp/WINGS/Models/Telemetry.cs new file mode 100644 index 0000000..98a77c1 --- /dev/null +++ b/aspnetapp/WINGS/Models/Telemetry.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace WINGS.Models +{ + + public class LatestTelemetry + { + public string LatestTelemetryTime {get; set;} + public List LatestTelemetryPackets {get; set;} + } + public class TelemetryPacket + { + public PacketInfo PacketInfo { get; set; } + public List Telemetries { get; set; } + } + public class PacketInfo + { + public string Id { get; set; } + public string Name { get; set; } + public bool IsRealtimeData { get; set; } + public bool IsRestricted { get; set; } + } + + public class Telemetry + { + public TelemetryInfo TelemetryInfo { get; set; } + public TelemetryValue TelemetryValue { get; set; } + } + + +public class TelemetryInfo + { + public string Name { get; set; } + public string Type { get; set; } + public string Unit { get; set; } + public int OctetPos { get; set; } + public int BitPos { get; set; } + public int BitLen { get; set; } + public string ConvType { get; set; } + public double[] Poly { get; set; } + public Dictionary Status { get; set; } + public string Description { get; set; } + } + + public class TelemetryValue + { + public string Time { get; set; } + public UInt32 TI { get; set; } + public string Value { get; set; } + public string RawValue { get; set; } + } + + public class TelemetryPacketHistory + { + public PacketInfo PacketInfo { get; set; } + public List TelemetryHistories { get; set; } + } + + public class TelemetryHistory + { + public TelemetryInfo TelemetryInfo { get; set; } + public List TelemetryValues { get; set; } + } + +} diff --git a/aspnetapp/WINGS/Models/TlmCmdFileConfig.cs b/aspnetapp/WINGS/Models/TlmCmdFileConfig.cs new file mode 100644 index 0000000..75c70a2 --- /dev/null +++ b/aspnetapp/WINGS/Models/TlmCmdFileConfig.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace WINGS.Models +{ + public enum TlmCmdFileLocation + { + Local + } + + public class TlmCmdFileLocationInfo + { + public string DirPath { get; set; } + } + + public class TlmCmdFileConfig + { + public TlmCmdFileLocation Location { get; set; } + public List CmdDBInfo { get; set; } + public List TlmDBInfo { get; set; } + public List CmdFileInfo { get; set; } + public TlmCmdFileLocationInfo LayoutInfo { get; set; } + } +} diff --git a/aspnetapp/WINGS/Models/TmtcPacket.cs b/aspnetapp/WINGS/Models/TmtcPacket.cs new file mode 100644 index 0000000..b81f62b --- /dev/null +++ b/aspnetapp/WINGS/Models/TmtcPacket.cs @@ -0,0 +1,13 @@ +namespace WINGS.Models +{ + public class TmPacketData + { + public string Opid { get; set; } + public byte[] TmPacket { get; set; } + } + public class TcPacketData + { + public string Opid { get; set; } + public byte[] TcPacket { get; set; } + } +} \ No newline at end of file diff --git a/aspnetapp/WINGS/Models/ZipItem.cs b/aspnetapp/WINGS/Models/ZipItem.cs new file mode 100644 index 0000000..45b2acd --- /dev/null +++ b/aspnetapp/WINGS/Models/ZipItem.cs @@ -0,0 +1,10 @@ +using System.IO; + +namespace WINGS.Models +{ + public class ZipItem + { + public string Name { get; set; } + public Stream Content { get; set; } + } +} diff --git a/aspnetapp/WINGS/Pages/Error.cshtml b/aspnetapp/WINGS/Pages/Error.cshtml new file mode 100644 index 0000000..eebfe10 --- /dev/null +++ b/aspnetapp/WINGS/Pages/Error.cshtml @@ -0,0 +1,26 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

    Error.

    +

    An error occurred while processing your request.

    + +@if (Model.ShowRequestId) +{ +

    + Request ID: @Model.RequestId +

    +} + +

    Development Mode

    +

    + Swapping to the Development environment displays detailed information about the error that occurred. +

    +

    + The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

    diff --git a/aspnetapp/WINGS/Pages/Error.cshtml.cs b/aspnetapp/WINGS/Pages/Error.cshtml.cs new file mode 100644 index 0000000..418b29c --- /dev/null +++ b/aspnetapp/WINGS/Pages/Error.cshtml.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace WINGS.Pages +{ + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public class ErrorModel : PageModel + { + private readonly ILogger _logger; + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} diff --git a/aspnetapp/WINGS/Pages/_ViewImports.cshtml b/aspnetapp/WINGS/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..2cad4a2 --- /dev/null +++ b/aspnetapp/WINGS/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using WINGS +@namespace WINGS.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/aspnetapp/WINGS/Program.cs b/aspnetapp/WINGS/Program.cs new file mode 100644 index 0000000..1be937d --- /dev/null +++ b/aspnetapp/WINGS/Program.cs @@ -0,0 +1,50 @@ +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Server.Kestrel.Core; + +namespace WINGS +{ + public class Program + { + public static void Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + using (var scope = host.Services.CreateScope()) + { + var serviceProvider = scope.ServiceProvider; + } + host.Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + // For development by MacOS + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + webBuilder.ConfigureKestrel(options => + { + // Setup a Http/1 endpoint without TLS + options.ListenLocalhost(5000, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1; + }); + // Setup a HTTP/2 endpoint without TLS for gRPC + options.ListenLocalhost(6000, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + } + webBuilder.UseStartup() + .UseDefaultServiceProvider(options => + options.ValidateScopes = false); + }); + } + } +} diff --git a/aspnetapp/WINGS/Properties/launchSettings.json b/aspnetapp/WINGS/Properties/launchSettings.json new file mode 100644 index 0000000..f5b4a19 --- /dev/null +++ b/aspnetapp/WINGS/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:20535", + "sslPort": 44304 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WINGS": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;https://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetapp/WINGS/Protos/tmtc.proto b/aspnetapp/WINGS/Protos/tmtc.proto new file mode 100644 index 0000000..a6d20f9 --- /dev/null +++ b/aspnetapp/WINGS/Protos/tmtc.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +option csharp_namespace = "WINGS.GrpcService"; + +package tmtc; + +service TmtcPacket { + rpc TmPacketTransfer (TmPacketDataRpc) returns (TmPacketResponseRpc); + rpc TcPacketTransfer (TcPacketRequestRpc) returns (stream TcPacketDataRpc); +} + +message TmPacketDataRpc { + string opid = 1; + bytes tmPacket = 2; +} + +message TmPacketResponseRpc { + string opid = 1; + bool ack = 2; +} + +message TcPacketRequestRpc { + string opid = 1; +} + +message TcPacketDataRpc { + string opid = 1; + bytes tcPacket = 2; +} diff --git a/aspnetapp/WINGS/Services/Core/CommandService.cs b/aspnetapp/WINGS/Services/Core/CommandService.cs new file mode 100644 index 0000000..4057d91 --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/CommandService.cs @@ -0,0 +1,420 @@ +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Globalization; +using WINGS.Data; +using WINGS.Models; +using Microsoft.EntityFrameworkCore; + +namespace WINGS.Services +{ + public class CommandService : ICommandService + { + private readonly ApplicationDbContext _dbContext; + private readonly ITmtcHandlerFactory _tmtcHandlerFactory; + private readonly ITcPacketManager _tcPacketManager; + private readonly IDbRepository _dbRepository; + private readonly ICommandFileRepository _fileRepository; + private readonly ICommandFileLogRepository _filelogRepository; + private static Dictionary> _indexesDict; + + static CommandService() + { + _indexesDict = new Dictionary>(); + } + + public CommandService(ApplicationDbContext dbContext, + ITmtcHandlerFactory tmtcHandlerFactory, + ITcPacketManager tcPacketManager, + IDbRepository dbRepository, + ICommandFileRepository fileRepository, + ICommandFileLogRepository filelogRepository) + { + _dbContext = dbContext; + _tmtcHandlerFactory = tmtcHandlerFactory; + _tcPacketManager = tcPacketManager; + _dbRepository = dbRepository; + _fileRepository = fileRepository; + _filelogRepository = filelogRepository; + } + + public IEnumerable GetAllCommand(string opid) + { + return _tcPacketManager.GetCommandDb(opid); + } + + public async Task SendCommandAsync(string opid, Command command, string commanderId) + { + var commandDb = new List(_tcPacketManager.GetCommandDb(opid)); + if (!IsParamsTypeCheckOk(command, commandDb)) return false; + + try + { + _tcPacketManager.RegisterCommand(opid, command); + + var commandLog = CommandToLog(opid, command); + _dbContext.CommandLogs.Add(commandLog); + await _dbContext.SaveChangesAsync(); + + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public void SendRawCommand(string opid, byte[] packet) + { + var data = new TcPacketData(){ + Opid = opid, + TcPacket = packet + }; + _tmtcHandlerFactory.GetTmtcPacketService(opid).Send(data); + } + + public async Task AddCmdFileLineLog(string opid, CommandFileLineLog command_file_line_log, string commanderName) + { + try + { + await _filelogRepository.AddHistoryAsync(opid, command_file_line_log, commanderName); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public IEnumerable GetCommandFileIndexes(string opid) + { + if (!_indexesDict.TryGetValue(opid, out var indexes)) + { + throw new ResourceNotFoundException("The operation is not running"); + } + return indexes; + } + + public async Task GetCommandFileAsync(string opid, int cmdFileInfoIndex, int fileId) + { + if (!_indexesDict.TryGetValue(opid, out var indexes)) + { + throw new ResourceNotFoundException("The operation is not running"); + } + var index = indexes.FirstOrDefault(index => index.CmdFileInfoIndex == cmdFileInfoIndex && index.FileId == fileId); + if (index == null) + { + throw new ResourceNotFoundException("The command file is not found"); + } + var config = await new TlmCmdFileConfigBuilder(_dbContext).Build(opid); + var commandDb = _tcPacketManager.GetCommandDb(opid); + var file = await _fileRepository.LoadCommandFileAsync(config, index, commandDb); + return file; + } + + public async Task ConfigureCommandDbAsync(Operation operation, TlmCmdFileConfig config) + { + try + { + var commandDb = await _dbRepository.LoadAllFilesAsync(config); + _tcPacketManager.SetCommandDb(operation.Id, commandDb.ToList()); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public async Task ConfigureCommandFileAsync(Operation operation, TlmCmdFileConfig config) + { + try + { + var indexes = await _fileRepository.LoadCommandFileIndexesAsync(config); + _indexesDict.Add(operation.Id, indexes); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public async Task ReconfigureCommandFileAsync(string opid) + { + var operation = await _dbContext.Operations.FindAsync(opid); + if (!operation.IsRunning || operation == null) + { + throw new ResourceNotFoundException("The operation is not running"); + } + _indexesDict.Remove(opid); + var config = await new TlmCmdFileConfigBuilder(_dbContext).Build(opid); + var indexes = await _fileRepository.LoadCommandFileIndexesAsync(config); + _indexesDict.Add(opid, indexes); + } + + public bool ConfigureCommandFileLog(Operation operation) + { + try + { + _filelogRepository.InitializeLogFiles(operation.Id); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public void RemoveCommandFileIndexes(string opid) + { + _indexesDict.Remove(opid); + } + + public Stream GetCommandLogStream(string opid) + { + var sb = new StringBuilder(); + sb.Append("Time, CmdName, Param1, Param2, Param3, Param4, Param5, Param6\r\n"); + + var commandLogs = _dbContext.CommandLogs + .Where(c => c.OperationId == opid) + .OrderBy(c => c.SentAt); + + foreach (var log in commandLogs) + { + sb.Append(log.SentAt.ToString("yyyy-MM-dd HH:mm:ss.f")+","+","+log.CmdName+","+log.Param1+","+log.Param2+","+log.Param3+","+log.Param4+","+log.Param5+","+log.Param6+"\r\n"); + } + var logByteArray = Encoding.UTF8.GetBytes(sb.ToString()); + return new MemoryStream(logByteArray); + } + + public IEnumerable GetCmdLogHistory (string opid) + { + return _filelogRepository.GetCmdLogHistory(opid); + } + public Stream GetCommandFileLogStream(string opid) + { + return _filelogRepository.GetLogFileStream(opid); + } + + private CommandLog CommandToLog(string opid, Command command) + { + var values = new string[6]; + for (int i = 0; i < 6; i++) + { + values[i] = i < command.Params.Count ? command.Params[i].Value.ToString() : null; + } + return new CommandLog + { + ExecType = command.ExecType, + ExecTime = command.ExecTime, + SentAt = DateTime.Now, + CmdName = command.Name, + OperationId = opid, + Param1 = values[0], + Param2 = values[1], + Param3 = values[2], + Param4 = values[3], + Param5 = values[4], + Param6 = values[5] + }; + } + + private bool IsParamsTypeCheckOk(Command commandWillBeExecuted, IEnumerable commandDb) + { + var command = commandWillBeExecuted.Clone(); // deep copy + + // Search cmd from DB by name-matching + var commandFromDb = commandDb.FirstOrDefault(cDb => (cDb.Code == command.Code && cDb.Component == command.Component)); + if (commandFromDb == null) + { + Console.WriteLine("\"Command\" : Command not found in CmdDB"); + return false; + } + + // Check match of Params.Count + if (commandFromDb.Params.Count != 0) + { + if (commandFromDb.Params[commandFromDb.Params.Count - 1].Type.ToLower() != "raw") + { + if (commandFromDb.Params.Count != command.Params.Count) + { + Console.WriteLine("\"Command\" : wrong number of parameters"); + return false; + } + } + } + else{ + if (command.Params.Count != 0) + { + Console.WriteLine("\"Command\" : wrong number of parameters"); + return false; + } + } + + // Check match of types of Params + NumberStyles style; + for (int SL = 0; SL < command.Params.Count; SL++) + { + if (command.Params[SL].Value.Contains("0x")) + { + style = NumberStyles.HexNumber; + command.Params[SL].Value = command.Params[SL].Value.Replace("0x", ""); //TryParseは0xがあると成功しない + } + else + { + style = NumberStyles.Integer; + } + switch (commandFromDb.Params[SL].Type.ToLower()) + { + //TODO: How to show the number of wrong-type parameters + case "int8_t": + case "int8": + SByte sbyte_val; + if (SByte.TryParse(command.Params[SL].Value, style, CultureInfo.InvariantCulture, out sbyte_val)) + { + if (style == NumberStyles.HexNumber) + { + command.Params[SL].Value = "0x" + command.Params[SL].Value; + } + } + else + { + Console.WriteLine("\"Command\" : wrong type of parameters"); + return false; + } + break; + case "uint8_t": + case "uint8": + Byte byte_val; + if (Byte.TryParse(command.Params[SL].Value, style, CultureInfo.InvariantCulture, out byte_val)) + { + if (style == NumberStyles.HexNumber) + { + command.Params[SL].Value = "0x" + command.Params[SL].Value; + } + } + else + { + Console.WriteLine("\"Command\" : wrong type of parameters"); + return false; + } + break; + case "int16_t": + case "int16": + Int16 int16_val; + if (Int16.TryParse(command.Params[SL].Value, style, CultureInfo.InvariantCulture, out int16_val)) + { + if (style == NumberStyles.HexNumber) + { + command.Params[SL].Value = "0x" + command.Params[SL].Value; + } + } + else + { + Console.WriteLine("\"Command\" : wrong type of parameters"); + return false; + } + break; + case "uint16_t": + case "uint16": + UInt16 uint16_val; + if (UInt16.TryParse(command.Params[SL].Value, style, CultureInfo.InvariantCulture, out uint16_val)) + { + if (style == NumberStyles.HexNumber) + { + command.Params[SL].Value = "0x" + command.Params[SL].Value; + } + } + else + { + Console.WriteLine("\"Command\" : wrong type of parameters"); + return false; + } + break; + case "int32_t": + case "int32": + Int32 int32_val; + if (Int32.TryParse(command.Params[SL].Value, style, CultureInfo.InvariantCulture, out int32_val)) + { + if (style == NumberStyles.HexNumber) + { + command.Params[SL].Value = "0x" + command.Params[SL].Value; + } + } + else + { + Console.WriteLine("\"Command\" : wrong type of parameters"); + return false; + } + break; + case "uint32_t": + case "uint32": + UInt32 uint32_val; + if (UInt32.TryParse(command.Params[SL].Value, style, CultureInfo.InvariantCulture, out uint32_val)) + { + if (style == NumberStyles.HexNumber) + { + command.Params[SL].Value = "0x" + command.Params[SL].Value; + } + } + else + { + Console.WriteLine("\"Command\" : wrong type of parameters"); + return false; + } + break; + case "float": + float float_val; + if (float.TryParse(command.Params[SL].Value, out float_val)) + { + } + else + { + Console.WriteLine("\"Command\" : wrong type of parameters"); + return false; + } + break; + case "double": + double double_val; + if (double.TryParse(command.Params[SL].Value, out double_val)) + { + } + else + { + Console.WriteLine("\"Command\" : wrong type of parameters"); + return false; + } + break; + case "raw": + if (style != NumberStyles.HexNumber) + { + Console.WriteLine("\"Command\" : The raw parameter should be HEX."); + return false; + } + for (int i = SL + 1; i < command.Params.Count; i++) + { + if (!command.Params[i].Value.Contains("0x")) + { + Console.WriteLine("\"Command\" : The raw parameter should be HEX."); + return false; + } + } + break; + default: + Console.WriteLine("\"Command\" : undefined type (check CMD_DB)"); + return false; + } + } + return true; + } + } +} diff --git a/aspnetapp/WINGS/Services/Core/Interfaces/ICommandService.cs b/aspnetapp/WINGS/Services/Core/Interfaces/ICommandService.cs new file mode 100644 index 0000000..8a16f0d --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/Interfaces/ICommandService.cs @@ -0,0 +1,25 @@ +using System.IO; +using System.Threading.Tasks; +using System.Collections.Generic; +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ICommandService + { + IEnumerable GetAllCommand(string opid); + Task SendCommandAsync(string opid, Command command, string commanderId); + void SendRawCommand(string opid, byte[] packet); + Task AddCmdFileLineLog(string opid, CommandFileLineLog command_file_line_log, string commanderId); + IEnumerable GetCommandFileIndexes(string opid); + Task GetCommandFileAsync(string opid, int cmdFileInfoIndex, int fileId); + Task ConfigureCommandDbAsync(Operation operation, TlmCmdFileConfig config); + Task ConfigureCommandFileAsync(Operation operation, TlmCmdFileConfig config); + bool ConfigureCommandFileLog(Operation operation); + Task ReconfigureCommandFileAsync(string opid); + void RemoveCommandFileIndexes(string opid); + Stream GetCommandLogStream(string opid); + IEnumerable GetCmdLogHistory (string opid); + Stream GetCommandFileLogStream(string opid); + } +} diff --git a/aspnetapp/WINGS/Services/Core/Interfaces/ILayoutService.cs b/aspnetapp/WINGS/Services/Core/Interfaces/ILayoutService.cs new file mode 100644 index 0000000..5c6aa43 --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/Interfaces/ILayoutService.cs @@ -0,0 +1,17 @@ +using System.IO; +using System.Threading.Tasks; +using System.Collections.Generic; +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ILayoutService + { + IEnumerable GetAllLayout(string opid); + Task ConfigureLayoutAsync(Operation operation, TlmCmdFileConfig config); + Task SaveLayoutAsync(string opid, string name, string lytStr); + Task RenameLayoutAsync(string opid, string name, int index); + Task DeleteLayoutAsync(string opid, string name); + void RemoveLayouts(string opid); + } +} diff --git a/aspnetapp/WINGS/Services/Core/Interfaces/IOperationService.cs b/aspnetapp/WINGS/Services/Core/Interfaces/IOperationService.cs new file mode 100644 index 0000000..4fd578a --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/Interfaces/IOperationService.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using System.Collections.Generic; +using WINGS.Models; + +namespace WINGS.Services +{ + public interface IOperationService + { + IEnumerable GetCurrentOperations(); + Task GetOperationByIdAsync(string opid); + Task> GetOperationHistoryAsync(int page, int size, string search); + Task StartOperationAsync(Operation operation); + Task CancelOperationAsync(string opid); + Task StopOperationAsync(string opid); + Task UpdateOperationHistoryAsync(Operation operation); + Task DeleteOperationHistoryAsync(string opid); + } +} diff --git a/aspnetapp/WINGS/Services/Core/Interfaces/ITelemetryService.cs b/aspnetapp/WINGS/Services/Core/Interfaces/ITelemetryService.cs new file mode 100644 index 0000000..fcc1f41 --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/Interfaces/ITelemetryService.cs @@ -0,0 +1,18 @@ +using System.IO; +using System.Threading.Tasks; +using System.Collections.Generic; +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ITelemetryService + { + LatestTelemetry GetLatestTelemetry(string opid, string refTlmTime); + IEnumerable GetTelemetryHistory(string opid); + List GetPacketsWithData(string opid); + List GetRecordPacketsWithData(string opid); + Task ConfigureTelemetryDbAsync(Operation operation, TlmCmdFileConfig config); + Stream GetTelemetryLogStream(string opid, List packetNames); + Stream GetRecordTelemetryLogStream(string opid, List packetNames); + } +} diff --git a/aspnetapp/WINGS/Services/Core/Interfaces/ITlmCmdFileConfigBuilder.cs b/aspnetapp/WINGS/Services/Core/Interfaces/ITlmCmdFileConfigBuilder.cs new file mode 100644 index 0000000..a3616ac --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/Interfaces/ITlmCmdFileConfigBuilder.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ITlmCmdFileConfigBuilder + { + Task Build(string opid); + } +} diff --git a/aspnetapp/WINGS/Services/Core/LayoutService.cs b/aspnetapp/WINGS/Services/Core/LayoutService.cs new file mode 100644 index 0000000..c7c7f43 --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/LayoutService.cs @@ -0,0 +1,122 @@ +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WINGS.Data; +using WINGS.Models; + +namespace WINGS.Services +{ + public class LayoutService : ILayoutService + { + private readonly ILayoutRepository _Repository; + private static Dictionary> _layoutDict; + private readonly ITlmCmdFileConfigBuilder _configBuilder; + + static LayoutService() + { + _layoutDict = new Dictionary>(); + } + + public LayoutService(ILayoutRepository Repository, + ITlmCmdFileConfigBuilder configBuilder) + { + _Repository = Repository; + _configBuilder = configBuilder; + } + + public IEnumerable GetAllLayout(string opid) + { + if (!_layoutDict.TryGetValue(opid, out var layouts)) + { + throw new ResourceNotFoundException("Layouts of this operation are not found"); + } + return layouts; + } + + public async Task ConfigureLayoutAsync(Operation operation, TlmCmdFileConfig config) + { + try + { + var layouts = await _Repository.LoadAllFilesAsync(config); + _layoutDict.Remove(operation.Id); + _layoutDict.Add(operation.Id, layouts.ToList()); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public async Task SaveLayoutAsync(string opid, string name, string lytStr) + { + /*int i; + for (i=0; i<_layoutDict[opid].Count;i++){ + if (name == _layoutDict[opid][i].name||name == ""){ + Console.WriteLine("invalid layout name"); + return false; + } + }*/ + try + { + var config = await _configBuilder.Build(opid); + _Repository.SaveLayoutAsync(config, name, lytStr); + var layouts = await _Repository.LoadAllFilesAsync(config); + _layoutDict.Remove(opid); + _layoutDict.Add(opid, layouts.ToList()); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public async Task RenameLayoutAsync(string opid, string name, int index) + { + try + { + var config = await _configBuilder.Build(opid); + var oldName = _layoutDict[opid][index].name; + _Repository.RenameLayoutAsync(config, name, oldName); + var layouts = await _Repository.LoadAllFilesAsync(config); + _layoutDict.Remove(opid); + _layoutDict.Add(opid, layouts.ToList()); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public async Task DeleteLayoutAsync(string opid, string name) + { + try + { + var config = await _configBuilder.Build(opid); + _Repository.DeleteLayoutAsync(config, name); + var layouts = await _Repository.LoadAllFilesAsync(config); + _layoutDict.Remove(opid); + _layoutDict.Add(opid, layouts.ToList()); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public void RemoveLayouts(string opid) + { + _layoutDict.Remove(opid); + } + } +} diff --git a/aspnetapp/WINGS/Services/Core/OperationService.cs b/aspnetapp/WINGS/Services/Core/OperationService.cs new file mode 100644 index 0000000..b702100 --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/OperationService.cs @@ -0,0 +1,227 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Hosting; +using WINGS.Data; +using WINGS.Models; +using WINGS.Library; + +namespace WINGS.Services +{ + public class OperationService : IOperationService + { + private readonly IWebHostEnvironment _env; + private readonly ApplicationDbContext _dbContext; + private readonly ITmtcHandlerFactory _tmtcHandlerFactory; + private readonly ITmPacketManager _tmPacketManager; + private readonly ITcPacketManager _tcPacketManager; + + public OperationService(IWebHostEnvironment env, + ApplicationDbContext dbContext, + ITmtcHandlerFactory tmtcHandlerFactory, + ITmPacketManager tmPacketManager, + ITcPacketManager tcPacketManager) + { + _env = env; + _dbContext = dbContext; + _tmtcHandlerFactory = tmtcHandlerFactory; + _tmPacketManager = tmPacketManager; + _tcPacketManager = tcPacketManager; + } + + public IEnumerable GetCurrentOperations() + { + return _dbContext.Operations + .Where(o => o.IsRunning) + .Include(o => o.Component); + } + + public async Task GetOperationByIdAsync(string opid) + { + var operation = await _dbContext.Operations.FindAsync(opid); + if (operation == null) + { + throw new ResourceNotFoundException("The operation is not found"); + } + return operation; + } + + public async Task> GetOperationHistoryAsync(int page, int size, string search) + { + var operations = await _dbContext.Operations + .Include(o => o.Component) + .Where(o => !o.IsRunning) + .ToListAsync(); + if (!String.IsNullOrEmpty(search)) + { + operations = operations.Where(o => o.Comment.Contains(search)).ToList(); + } + + var totalCount = operations.Count(); + var baseUrl = "/api/operations/history"; + var query = new Dictionary(){{"search", search}}; + var meta = Paginator.GetPageMeta(baseUrl, page, size, totalCount, query); + + var offset = (page - 1) * size; + var data = operations + .OrderByDescending(o => o.CreatedAt) + .Where((o,i) => (offset <= i && i < offset+size)) + .ToList(); + + return new Pagination{ + Meta = meta, + Data = data + }; + } + + public async Task StartOperationAsync(Operation operation) + { + string opid = Guid.NewGuid().ToString("d"); + + if (operation.TmtcTarget == TmtcTarget.Infostellar && InfostellarOperationRunning()) + { + throw new IllegalContextException("The Infostellar operation is limited to one"); + } + + try + { + operation.Id = opid; + operation.CreatedAt = DateTime.Now; + operation.IsRunning = false; + operation.IsTmtcConnected = false; + _dbContext.Operations.Add(operation); + await _dbContext.SaveChangesAsync(); + } + catch + { + throw new ResourceCreateException("Cannot create new entity"); + } + + var component = await _dbContext.Components.FindAsync(operation.ComponentId); + if (component == null) + { + throw new ResourceNotFoundException("The component is not found"); + } + + _tmtcHandlerFactory.AddOperation(opid, component, operation.TmtcTarget); + CreateLogDirectory(opid); + + operation.IsRunning = true; + await UpdateOperationHistoryAsync(operation); + + return operation; + } + + public async Task CancelOperationAsync(string opid) + { + var operation = await _dbContext.Operations.FindAsync(opid); + if (operation == null) + { + throw new ResourceNotFoundException("The operation is not found"); + } + + operation.IsRunning = false; + await UpdateOperationHistoryAsync(operation); + + try + { + _tmPacketManager.RemoveOperation(opid); + _tcPacketManager.RemoveOperation(opid); + _tmtcHandlerFactory.RemoveOperation(opid); + } + catch + { + // If this operation is canceled before registered to tm/tc packet manager + // an execption is caught here, so there's no need to handle. + } + DeleteLogFiles(opid); + } + + public async Task StopOperationAsync(string opid) + { + var operation = await _dbContext.Operations.FindAsync(opid); + if (operation == null) + { + throw new ResourceNotFoundException("The operation is not found"); + } + + try + { + _tmPacketManager.RemoveOperation(opid); + _tcPacketManager.RemoveOperation(opid); + _tmtcHandlerFactory.RemoveOperation(opid); + } + catch + { + // If this operation is added to database before wings is started + // an execption is caught here, so there's no need to handle. + } + + operation.IsRunning = false; + await UpdateOperationHistoryAsync(operation); + } + + public async Task UpdateOperationHistoryAsync(Operation operation) + { + try + { + _dbContext.Entry(operation).State = EntityState.Modified; + await _dbContext.SaveChangesAsync(); + } + catch + { + if (!OperationExists(operation.Id)) + { + throw new ResourceNotFoundException("The operation is not found"); + } + throw new ResourceUpdateException("Cannot update entity"); + } + } + + public async Task DeleteOperationHistoryAsync(string opid) + { + var operation = await _dbContext.Operations.FindAsync(opid); + if (operation.IsRunning) + { + throw new IllegalContextException("Cannot delete running operation logs"); + } + + try + { + _dbContext.Operations.Remove(operation); + await _dbContext.SaveChangesAsync(); + } + catch + { + if (!OperationExists(opid)) + { + throw new ResourceNotFoundException("The operation is not found"); + } + throw new ResourceDeleteException("Cannot delete entity"); + } + + DeleteLogFiles(opid); + } + + private bool OperationExists(string opid) => + _dbContext.Operations.Any(o => o.Id == opid); + + private bool InfostellarOperationRunning() => + GetCurrentOperations().Any(o => o.TmtcTarget == TmtcTarget.Infostellar); + + private void CreateLogDirectory(string opid) + { + string path = Path.Combine(_env.ContentRootPath, "Logs", opid); + Directory.CreateDirectory(path); + } + + private void DeleteLogFiles(string opid) + { + string path = Path.Combine(_env.ContentRootPath, "Logs", opid); + Directory.Delete(path, true); + } + } +} diff --git a/aspnetapp/WINGS/Services/Core/TelemetryService.cs b/aspnetapp/WINGS/Services/Core/TelemetryService.cs new file mode 100644 index 0000000..9830876 --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/TelemetryService.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WINGS.Data; +using WINGS.Models; +using WINGS.Library; + +namespace WINGS.Services +{ + public class TelemetryService : ITelemetryService + { + private readonly ITmPacketManager _tmPacketManager; + private readonly ITelemetryLogRepository _logRepository; + private readonly IDbRepository _dbRepository; + + public TelemetryService(ITmPacketManager tmPacketManager, + ITelemetryLogRepository logRepository, + IDbRepository dbRepository) + { + _tmPacketManager = tmPacketManager; + _logRepository = logRepository; + _dbRepository = dbRepository; + } + + public LatestTelemetry GetLatestTelemetry(string opid, string refTlmTime) + { + return _tmPacketManager.GetLatestTelemetry(opid, refTlmTime); + } + + public IEnumerable GetTelemetryHistory(string opid) + { + return _logRepository.GetTelemetryHistory(opid, _tmPacketManager.GetTelemetryDb(opid)); + } + + public List GetPacketsWithData(string opid) + { + return _logRepository.GetPacketsWithData(opid); + } + public List GetRecordPacketsWithData(string opid) + { + return _logRepository.GetRecordPacketsWithData(opid); + } + + public async Task ConfigureTelemetryDbAsync(Operation operation, TlmCmdFileConfig config) + { + try + { + var telemetryDb = await _dbRepository.LoadAllFilesAsync(config); + _tmPacketManager.SetTelemetryDb(operation.Id, telemetryDb.ToList()); + _logRepository.InitializeLogFiles(operation.Id, telemetryDb.ToList()); + return true; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + } + + public Stream GetTelemetryLogStream(string opid, List packetNames) + { + var zipItems = new List(); + foreach(var packetName in packetNames) + { + zipItems.Add(new ZipItem(){ + Name = packetName + ".csv", + Content = _logRepository.GetLogFileStream(opid, packetName) + }); + } + return Zipper.GetZipStream(zipItems); + } + public Stream GetRecordTelemetryLogStream(string opid, List packetNames) + { + var zipItems = new List(); + foreach(var packetName in packetNames) + { + zipItems.Add(new ZipItem(){ + Name = packetName + ".csv", + Content = _logRepository.GetRecordLogFileStream(opid, packetName) + }); + } + return Zipper.GetZipStream(zipItems); + } + } +} diff --git a/aspnetapp/WINGS/Services/Core/TlmCmdFileConfigBuilder.cs b/aspnetapp/WINGS/Services/Core/TlmCmdFileConfigBuilder.cs new file mode 100644 index 0000000..b1817f4 --- /dev/null +++ b/aspnetapp/WINGS/Services/Core/TlmCmdFileConfigBuilder.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using WINGS.Data; +using WINGS.Models; + +namespace WINGS.Services +{ + public class TlmCmdFileConfigBuilder : ITlmCmdFileConfigBuilder + { + private readonly ApplicationDbContext _dbContext; + + public TlmCmdFileConfigBuilder(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + /// + /// Returns the file config of the specified operation + /// + /// Operation id + public async Task Build(string opid) + { + var operation = await _dbContext.Operations + .Include(o => o.Component) + .FirstOrDefaultAsync(o => o.Id == opid); + + var config = new TlmCmdFileConfig(){ + Location = operation.FileLocation, + CmdDBInfo = new List(), + TlmDBInfo = new List(), + CmdFileInfo = new List(), + LayoutInfo = new TlmCmdFileLocationInfo() + }; + + switch (config.Location) + { + case TlmCmdFileLocation.Local: + config.CmdDBInfo.Add(new TlmCmdFileLocationInfo() { DirPath = operation.Component.LocalDirPath }); + config.TlmDBInfo.Add(new TlmCmdFileLocationInfo() { DirPath = operation.Component.LocalDirPath }); + config.CmdFileInfo.Add(new TlmCmdFileLocationInfo() { DirPath = operation.Component.LocalDirPath }); + config.LayoutInfo = new TlmCmdFileLocationInfo() { DirPath = operation.Component.LocalDirPath }; + break; + + default: + throw new NotImplementedException("Undefined file location"); + } + + return config; + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITcPacketManager.cs b/aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITcPacketManager.cs new file mode 100644 index 0000000..d3a0aaa --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITcPacketManager.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ITcPacketManager + { + void SetCommandDb(string opid, List commandDb); + void RemoveOperation(string opid); + List GetCommandDb(string opid); + void RegisterCommand(string opid, Command command); + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITmPacketManager.cs b/aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITmPacketManager.cs new file mode 100644 index 0000000..cccfa32 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITmPacketManager.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ITmPacketManager + { + void RemoveOperation(string opid); + void SetTelemetryDb(string opid, List telemetryDb); + List GetTelemetryDb(string opid); + LatestTelemetry GetLatestTelemetry(string opid, string refTlmTime); + Task RegisterTelemetryAsync(TmPacketData data); + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITmtcHandlerFactory.cs b/aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITmtcHandlerFactory.cs new file mode 100644 index 0000000..b96fa58 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Manager/Interfaces/ITmtcHandlerFactory.cs @@ -0,0 +1,13 @@ +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ITmtcHandlerFactory + { + ITmPacketAnalyzer GetTmPacketAnalyzer(string opid); + ITcPacketGenerator GetTcPacketGenerator(string opid); + ITmtcPacketService GetTmtcPacketService(string opid); + void AddOperation(string opid, Component component, TmtcTarget target); + void RemoveOperation(string opid); + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Manager/TcPacketManager.cs b/aspnetapp/WINGS/Services/TMTC/Manager/TcPacketManager.cs new file mode 100644 index 0000000..43b6e21 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Manager/TcPacketManager.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using WINGS.Models; + +namespace WINGS.Services +{ + public class TcPacketManager : ITcPacketManager + { + private readonly ITmtcHandlerFactory _tmtcHandlerFactory; + private Dictionary> _commandDbDict; + + public TcPacketManager(ITmtcHandlerFactory tmtcHandlerFactory) + { + _tmtcHandlerFactory = tmtcHandlerFactory; + _commandDbDict = new Dictionary>(); + } + + public void SetCommandDb(string opid, List commandDb) + { + _commandDbDict.Add(opid, commandDb); + } + + public void RemoveOperation(string opid) + { + _commandDbDict.Remove(opid); + } + + public List GetCommandDb(string opid) + { + if (!_commandDbDict.TryGetValue(opid, out var commandDb)) + { + throw new ResourceNotFoundException("The command db of this operation is not found"); + } + return commandDb; + } + + public void RegisterCommand(string opid, Command command) + { + var data = _tmtcHandlerFactory.GetTcPacketGenerator(opid).GetTcPacketData(opid, command); + _tmtcHandlerFactory.GetTmtcPacketService(opid).Send(data); + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Manager/TmPacketManager.cs b/aspnetapp/WINGS/Services/TMTC/Manager/TmPacketManager.cs new file mode 100644 index 0000000..1c4f09c --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Manager/TmPacketManager.cs @@ -0,0 +1,127 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Models; +using WINGS.Data; + +namespace WINGS.Services +{ + public class TmPacketManager : ITmPacketManager + { + private readonly ITmtcHandlerFactory _tmtcHandlerFactory; + private Dictionary> _telemetryDbDict; + private Dictionary> _latestTelemetryDict; + + public TmPacketManager(ITmtcHandlerFactory tmtcHandlerFactory) + { + _tmtcHandlerFactory = tmtcHandlerFactory; + _telemetryDbDict = new Dictionary>(); + _latestTelemetryDict = new Dictionary>(); + } + + public void RemoveOperation(string opid) + { + _tmtcHandlerFactory.GetTmPacketAnalyzer(opid).RemoveOperation(opid); + _telemetryDbDict.Remove(opid); + _latestTelemetryDict.Remove(opid); + } + + public void SetTelemetryDb(string opid, List telemetryDb) + { + _telemetryDbDict.Add(opid, telemetryDb); + _latestTelemetryDict.Add(opid, telemetryDb); + } + + public List GetTelemetryDb(string opid) + { + if (!_telemetryDbDict.TryGetValue(opid, out var telemetryDb)) + { + throw new ResourceNotFoundException("The telemetry db of this operation is not found"); + } + return telemetryDb; + } + + public LatestTelemetry GetLatestTelemetry(string opid, string refTlmTime) + { + if (!_latestTelemetryDict.TryGetValue(opid, out var telemetryPackets)) + { + throw new ResourceNotFoundException("The telemetry of this operation is not found"); + } + var telemetryPacketsRealtimeOnly = telemetryPackets.FindAll(packet => packet.PacketInfo.IsRealtimeData == true); + + var latestTelemetry = new LatestTelemetry(); + List latestTelemetryPackets = new List(telemetryPacketsRealtimeOnly); + List tlmTimeList = new List(); + CultureInfo ci = CultureInfo.CurrentCulture; + DateTimeStyles dts = DateTimeStyles.None; + DateTime refTlmDateTime; + DateTime tlmDateTime; + + if (DateTime.TryParseExact(refTlmTime, "yyyy-MM-dd HH:mm:ss.f", ci, dts, out refTlmDateTime)) + { + var tlmPackets = new List(); + + foreach (var telemetryPacket in latestTelemetryPackets) + { + var tlmTime = telemetryPacket.Telemetries[0].TelemetryValue.Time; + if (tlmTime == null) + { + continue; + } + else + { + if (DateTime.TryParseExact(tlmTime, "yyyy-MM-dd HH:mm:ss.f", ci, dts, out tlmDateTime)) + { + if(tlmDateTime > refTlmDateTime) + { + tlmPackets.Add(telemetryPacket); + } + } + } + } + latestTelemetry.LatestTelemetryPackets = tlmPackets; + } + else + { + latestTelemetry.LatestTelemetryPackets = latestTelemetryPackets; + } + + foreach (var telemetryPacket in latestTelemetryPackets) + { + var tlmTime = telemetryPacket.Telemetries[0].TelemetryValue.Time; + if (tlmTime == null) + { + continue; + } + else + { + if (DateTime.TryParseExact(tlmTime, "yyyy-MM-dd HH:mm:ss.f", ci, dts, out tlmDateTime)) + { + tlmTimeList.Add(tlmDateTime); + } + } + } + latestTelemetry.LatestTelemetryTime = (tlmTimeList.Count()!=0)?tlmTimeList.Max().ToString("yyyy-MM-dd HH:mm:ss.f"):""; + + return latestTelemetry; + } + + public async Task RegisterTelemetryAsync(TmPacketData data) + { + var opid = data.Opid; + var latestTelemetry = GetLatestTelemetryToUpdate(opid); + await _tmtcHandlerFactory.GetTmPacketAnalyzer(opid).AnalyzePacketAsync(data, latestTelemetry); + } + + private List GetLatestTelemetryToUpdate(string opid) + { + if (!_latestTelemetryDict.TryGetValue(opid, out var telemetryPackets)) + { + throw new ResourceNotFoundException("The telemetry of this operation is not found"); + } + return telemetryPackets; + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Manager/TmtcHandlerFactory.cs b/aspnetapp/WINGS/Services/TMTC/Manager/TmtcHandlerFactory.cs new file mode 100644 index 0000000..c29f579 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Manager/TmtcHandlerFactory.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using WINGS.Models; +using WINGS.Services.TmtcIf; + +namespace WINGS.Services +{ + public class TmtcHandlerFactory : ITmtcHandlerFactory + { + private readonly IServiceProvider _serviceProvider; + private Dictionary _componentDict; + private Dictionary _targetDict; + + public TmtcHandlerFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _componentDict = new Dictionary(); + _targetDict = new Dictionary(); + } + + public ITmPacketAnalyzer GetTmPacketAnalyzer(string opid) + { + if (!_componentDict.TryGetValue(opid, out var component)) + { + throw new ResourceNotFoundException("The component is not found"); + } + switch (component.TmPacketKey) + { + case "OBC": + return _serviceProvider.GetService(); + + case "ISSL_COMMON": + return _serviceProvider.GetService(); + + default: + throw new NotImplementedException("The TmPacketKey is not defined"); + } + } + + public ITcPacketGenerator GetTcPacketGenerator(string opid) + { + if (!_componentDict.TryGetValue(opid, out var component)) + { + throw new ResourceNotFoundException("The component is not found"); + } + switch (component.TcPacketKey) + { + case "OBC": + return _serviceProvider.GetService(); + + case "ISSL_COMMON": + return _serviceProvider.GetService(); + + default: + throw new NotImplementedException("The TcPacketKey is not defined"); + } + } + + public ITmtcPacketService GetTmtcPacketService(string opid) + { + if (!_targetDict.TryGetValue(opid, out var target)) + { + throw new ResourceNotFoundException("The component is not found"); + } + switch (target) + { + case TmtcTarget.TmtcIf: + return _serviceProvider.GetService(); + + default: + throw new NotImplementedException("The target is not found"); + } + } + + public void AddOperation(string opid, Component component, TmtcTarget target) + { + _componentDict.Add(opid, component); + _targetDict.Add(opid, target); + } + + public void RemoveOperation(string opid) + { + _componentDict.Remove(opid); + _targetDict.Remove(opid); + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Processor/Abstracts/TcPacketGeneratorBase.cs b/aspnetapp/WINGS/Services/TMTC/Processor/Abstracts/TcPacketGeneratorBase.cs new file mode 100644 index 0000000..95455a0 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Processor/Abstracts/TcPacketGeneratorBase.cs @@ -0,0 +1,221 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Data; +using WINGS.Models; + +namespace WINGS.Services +{ + public abstract class TcPacketGeneratorBase + { + protected abstract byte[] GeneratePacket(Command command); + + public TcPacketData GetTcPacketData(string opid, Command command) + { + var packet = GeneratePacket(command); + return new TcPacketData(){ + Opid = opid, + TcPacket = packet + }; + } + + protected int GetParamsByteLength(Command command) + { + int length = 0; + foreach (var param in command.Params) + { + length += param.Type switch + { + "uint8_t" => sizeof(Byte), + "uint8" => sizeof(Byte), + + "int8_t" => sizeof(SByte), + "int8" => sizeof(SByte), + + "uint16_t" => sizeof(UInt16), + "uint16" => sizeof(UInt16), + + "int16_t" => sizeof(Int16), + "int16" => sizeof(Int16), + + "uint32_t" => sizeof(UInt32), + "uint32" => sizeof(UInt32), + + "int32_t" => sizeof(Int32), + "int32" => sizeof(Int32), + + "float" => sizeof(Single), + "double" => sizeof(Double), + + "raw" => GetRawLength(param.Value), + + _ => throw new Exception("Undefined data type") + }; + } + return length; + } + + protected int GetRawLength(String val){ + val = val.Replace("0x", ""); + val = val.Replace("/", ""); + val = val.Replace("[", ""); + val = val.Replace("]", ""); + if (val.Length % 2 == 0) + { + return val.Length / 2; + } + else + { + throw new Exception("The value of \"raw\" should be in bytes."); + } + } + + protected void SetParams(byte[] packet, List commandParams, int offset) + { + int pos = offset; + foreach (var param in commandParams) + { + switch (param.Type) + { + case "uint8_t": + case "uint8": + { + Byte val; + if(param.Value.Contains("0x")){ + var param8bit = Convert.ToByte(param.Value, 16); + val = Convert.ToByte(param8bit); + } + else{ + val = Convert.ToByte(param.Value); + } + packet[pos] = val; + pos += sizeof(Byte); + break; + } + case "int8_t": + case "int8": + { + SByte val; + if(param.Value.Contains("0x")){ + var param8bit = Convert.ToSByte(param.Value, 16); + val = Convert.ToSByte(param8bit); + } + else{ + val = Convert.ToSByte(param.Value); + } + packet[pos] = (byte)val; + pos += sizeof(SByte); + break; + } + case "uint16_t": + case "uint16": + { + UInt16 val; + if(param.Value.Contains("0x")){ + var param16bit = Convert.ToUInt16(param.Value, 16); + val = Convert.ToUInt16(param16bit); + } + else{ + val = Convert.ToUInt16(param.Value); + } + packet[pos] = (byte)(val >> 8 & 0xff); + packet[pos+1] = (byte)(val & 0xff); + pos += sizeof(UInt16); + break; + } + case "int16_t": + case "int16": + { + Int16 val; + if(param.Value.Contains("0x")){ + var param16bit = Convert.ToInt16(param.Value, 16); + val = Convert.ToInt16(param16bit); + } + else{ + val = Convert.ToInt16(param.Value); + } + packet[pos] = (byte)(val >> 8 & 0xff); + packet[pos+1] = (byte)(val & 0xff); + pos += sizeof(Int16); + break; + } + case "uint32_t": + case "uint32": + { + UInt32 val; + if(param.Value.Contains("0x")){ + var param32bit = Convert.ToUInt32(param.Value, 16); + val = Convert.ToUInt32(param32bit); + } + else{ + val = Convert.ToUInt32(param.Value); + } + packet[pos] = (byte)(val >> 24 & 0xff); + packet[pos+1] = (byte)(val >> 16 & 0xff); + packet[pos+2] = (byte)(val >> 8 & 0xff); + packet[pos+3] = (byte)(val & 0xff); + pos += sizeof(UInt32); + break; + } + case "int32_t": + case "int32": + { + Int32 val; + if(param.Value.Contains("0x")){ + var param32bit = Convert.ToInt32(param.Value, 16); + val = Convert.ToInt32(param32bit); + } + else{ + val = Convert.ToInt32(param.Value); + } + packet[pos] = (byte)(val >> 24 & 0xff); + packet[pos+1] = (byte)(val >> 16 & 0xff); + packet[pos+2] = (byte)(val >> 8 & 0xff); + packet[pos+3] = (byte)(val & 0xff); + pos += sizeof(Int32); + break; + } + case "float": + { + Single val = Convert.ToSingle(param.Value); + var bytes = BitConverter.GetBytes(val); + for (int i = 0; i < sizeof(Single); i++) + { + packet[pos+i] = bytes[sizeof(Single)-i-1]; // To big endian + } + pos += sizeof(Single); + break; + } + case "double": + { + Double val = Convert.ToDouble(param.Value); + var bytes = BitConverter.GetBytes(val); + for (int i = 0; i < sizeof(Double); i++) + { + packet[pos+i] = bytes[sizeof(Double)-i-1]; // To big endian + } + pos += sizeof(Double); + break; + } + case "raw": + { + int rawlen = GetRawLength(param.Value); + param.Value = param.Value.Replace("0x", ""); + param.Value = param.Value.Replace("/", ""); + param.Value = param.Value.Replace("[", ""); + param.Value = param.Value.Replace("]", ""); + for (int i = 0; i < rawlen; i++) + { + packet[pos+i] = Convert.ToByte(param.Value.Substring(i*2, 2), 16); + } + pos += rawlen; + break; + } + default: + throw new Exception("Undefined data type"); + } + } + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Processor/Abstracts/TmPacketAnalyzerBase.cs b/aspnetapp/WINGS/Services/TMTC/Processor/Abstracts/TmPacketAnalyzerBase.cs new file mode 100644 index 0000000..af43ad5 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Processor/Abstracts/TmPacketAnalyzerBase.cs @@ -0,0 +1,312 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Data; +using WINGS.Models; + +namespace WINGS.Services +{ + public abstract class TmPacketAnalyzerBase + { + private readonly ITelemetryLogRepository _logRepository; + + public TmPacketAnalyzerBase(ITelemetryLogRepository logRepository) + { + _logRepository = logRepository; + } + + public abstract Task AnalyzePacketAsync(TmPacketData data, List prevTelemetry); + public virtual void RemoveOperation(string opid) + { + } + + protected async Task SetTelemetryListValuesAsync(List dataList, List packetIdList, List realtimeFlagList, List TIList, List prevTelemetry) + { + for (int i = 0; i < dataList.Count(); i++) + { + await SetTelemetryValuesAsync(dataList[i], packetIdList[i], realtimeFlagList[i], TIList[i], prevTelemetry); + } + return true; + } + + protected async Task SetTelemetryValuesAsync(TmPacketData data, string packetId, bool isRealtimeData, UInt32 TI, List prevTelemetry) + { + var opid = data.Opid; + var target = prevTelemetry.FirstOrDefault(packet => (packet.PacketInfo.Id == packetId) && (packet.PacketInfo.IsRealtimeData == isRealtimeData)); + if (target == null && isRealtimeData == false) // case for the first time of recordtlm packet + { + var targetRealtime = prevTelemetry.FirstOrDefault(packet => (packet.PacketInfo.Id == packetId) && (packet.PacketInfo.IsRealtimeData == true)); + + var packetInfo = new PacketInfo(){ + Id = targetRealtime.PacketInfo.Id, + Name = targetRealtime.PacketInfo.Name, + IsRealtimeData = false + }; + var telemetries = new List(targetRealtime.Telemetries); + foreach (var telemetry in telemetries) + { + telemetry.TelemetryValue = new TelemetryValue(); + } + target = new TelemetryPacket(){ + PacketInfo = packetInfo, + Telemetries = telemetries + }; + + prevTelemetry.Add(target); + } + var receivedTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.f"); + foreach (var tlm in target.Telemetries) + { + tlm.TelemetryValue.Time = receivedTime; + tlm.TelemetryValue.TI = TI; + try + { + SetValue(tlm, data.TmPacket); + } + catch + { + return false; + } + } + await _logRepository.AddHistoryAsync(opid, target); + return true; + } + + private void SetValue(Telemetry tlm, byte[] packet) + { + if (TypeTlmLenCheck(tlm.TelemetryInfo.Type, tlm.TelemetryInfo.BitLen)) + { + switch (tlm.TelemetryInfo.Type) + { + case "uint8_t": + case "uint8": + { + Byte raw = (Byte)packet[tlm.TelemetryInfo.OctetPos]; + tlm.TelemetryValue.Value = ConvertValue(raw, tlm); + tlm.TelemetryValue.RawValue = raw.ToString(); + break; + } + case "int8_t": + case "int8": + { + SByte raw = (SByte)packet[tlm.TelemetryInfo.OctetPos]; + tlm.TelemetryValue.Value = ConvertValue(raw, tlm); + tlm.TelemetryValue.RawValue = raw.ToString(); + break; + } + case "uint16_t": + case "uint16": + { + UInt16 raw = (UInt16)(packet[tlm.TelemetryInfo.OctetPos] << 8 | packet[tlm.TelemetryInfo.OctetPos+1]); + tlm.TelemetryValue.Value = ConvertValue(raw, tlm); + tlm.TelemetryValue.RawValue = raw.ToString(); + break; + } + case "int16_t": + case "int16": + { + Int16 raw = (Int16)(packet[tlm.TelemetryInfo.OctetPos] << 8 | packet[tlm.TelemetryInfo.OctetPos+1]); + tlm.TelemetryValue.Value = ConvertValue(raw, tlm); + tlm.TelemetryValue.RawValue = raw.ToString(); + break; + } + case "uint32_t": + case "uint32": + { + UInt32 raw = (UInt32)(packet[tlm.TelemetryInfo.OctetPos] << 24 | packet[tlm.TelemetryInfo.OctetPos+1] << 16 | packet[tlm.TelemetryInfo.OctetPos+2] << 8 | packet[tlm.TelemetryInfo.OctetPos+3]); + tlm.TelemetryValue.Value = ConvertValue(raw, tlm); + tlm.TelemetryValue.RawValue = raw.ToString(); + break; + } + case "int32_t": + case "int32": + { + Int32 raw = (Int32)(packet[tlm.TelemetryInfo.OctetPos] << 24 | packet[tlm.TelemetryInfo.OctetPos+1] << 16 | packet[tlm.TelemetryInfo.OctetPos+2] << 8 | packet[tlm.TelemetryInfo.OctetPos+3]); + tlm.TelemetryValue.Value = ConvertValue(raw, tlm); + tlm.TelemetryValue.RawValue = raw.ToString(); + break; + } + case "float": + { + var temp = new byte[]{packet[tlm.TelemetryInfo.OctetPos+3], packet[tlm.TelemetryInfo.OctetPos+2], packet[tlm.TelemetryInfo.OctetPos+1], packet[tlm.TelemetryInfo.OctetPos]}; + Single raw = BitConverter.ToSingle(temp, 0); + tlm.TelemetryValue.Value = ConvertValue(raw, tlm); + tlm.TelemetryValue.RawValue = raw.ToString(); + break; + } + case "double": + { + var temp = new byte[]{packet[tlm.TelemetryInfo.OctetPos+7], packet[tlm.TelemetryInfo.OctetPos+6], packet[tlm.TelemetryInfo.OctetPos+5], packet[tlm.TelemetryInfo.OctetPos+4], + packet[tlm.TelemetryInfo.OctetPos+3], packet[tlm.TelemetryInfo.OctetPos+2], packet[tlm.TelemetryInfo.OctetPos+1], packet[tlm.TelemetryInfo.OctetPos]}; + Double raw = BitConverter.ToDouble(temp, 0); + tlm.TelemetryValue.Value = ConvertValue(raw, tlm); + tlm.TelemetryValue.RawValue = raw.ToString(); + break; + } + default: + tlm.TelemetryValue.Value = "Error"; + tlm.TelemetryValue.RawValue = "Error"; + break; + } + } + else + { + // int bytenum = (tlm.TelemetryInfo.BitPos + tlm.TelemetryInfo.BitLen - 1) / 8 + 1; + Byte defraw; + Byte mask; + mask = (Byte)(((1 << tlm.TelemetryInfo.BitLen) - 1) << (8 - tlm.TelemetryInfo.BitPos - tlm.TelemetryInfo.BitLen)); + defraw = (Byte)((UInt16)(packet[tlm.TelemetryInfo.OctetPos] & mask) >> (8 - tlm.TelemetryInfo.BitPos - tlm.TelemetryInfo.BitLen)); + // support bytenum > 1 (e.g.: BitPos = 0, 8 < BitLen < 15) + /* + if (bytenum != 1) + { + var buf = (tlm.TelemetryInfo.BitLen - (8 - tlm.TelemetryInfo.BitPos)) % 8 + 1; + for (var i = 0; i < bytenum ; i++) + { + if (i == 0) + { + mask = (Byte)((1 << (8 - tlm.TelemetryInfo.BitPos)) - 1); + defraw += ((packet[tlm.TelemetryInfo.OctetPos] & mask) << (8 * (bytenum - 1))) >> (8 - buf); + } + else if (i == bytenum - 1) + { + mask = (Byte)(((1 << buf) - 1) << (8 - buf)); + defraw += ((packet[tlm.TelemetryInfo.OctetPos+i] & mask) << (8 * (bytenum - i))) >> (8 - buf); + } + else + { + mask = 0b_1111_1111; + defraw += ((packet[tlm.TelemetryInfo.OctetPos+i] & mask) << (8 * (bytenum - i - 1))) >> (8 - buf); + } + } + } + */ + tlm.TelemetryValue.Value = ConvertValue(defraw, tlm); + tlm.TelemetryValue.RawValue = defraw.ToString(); + } + } + + private string ConvertValue(T raw, Telemetry tlm) + { + switch (tlm.TelemetryInfo.ConvType) + { + case "NONE": + return raw.ToString(); + + case "POLY": + double x = Convert.ToDouble(raw); + double value = 0; + for (var i=0; i<6; i++) + { + value += tlm.TelemetryInfo.Poly[i]*Math.Pow(x,i); + } + return value.ToString(); + + case "STATUS": + var rawStr = raw.ToString(); + if (tlm.TelemetryInfo.Status.ContainsKey(rawStr)) + { + return tlm.TelemetryInfo.Status[rawStr]; + } + else if (tlm.TelemetryInfo.Status.ContainsKey("*")) + { + return tlm.TelemetryInfo.Status["*"]; + } + else + { + return "Undefined(" + rawStr +")"; + } + + case "HEX": + return HexConvertValue(raw, tlm); + + default: + throw new Exception("Undefined conversion type"); + } + } + + private string HexConvertValue(T raw, Telemetry tlm) + { + string hexraw; + switch (tlm.TelemetryInfo.Type) + { + case "uint8_t": + case "uint8": + { + Byte byteraw = Convert.ToByte(raw); + hexraw = "0x" + byteraw.ToString("x2"); + return hexraw; + } + case "int8_t": + case "int8": + { + SByte sbyteraw = Convert.ToSByte(raw); + hexraw = "0x" + sbyteraw.ToString("x2"); + return hexraw; + } + case "uint16_t": + case "uint16": + { + UInt16 uint16raw = Convert.ToUInt16(raw); + hexraw = "0x" + uint16raw.ToString("x4"); + return hexraw; + } + case "int16_t": + case "int16": + { + Int16 int16raw = Convert.ToInt16(raw); + hexraw = "0x" + int16raw.ToString("x4"); + return hexraw; + } + case "uint32_t": + case "uint32": + { + UInt32 uint32raw = Convert.ToUInt32(raw); + hexraw = "0x" + uint32raw.ToString("x8"); + return hexraw; + } + case "int32_t": + case "int32": + { + Int32 int32raw = Convert.ToInt32(raw); + hexraw = "0x" + int32raw.ToString("x8"); + return hexraw; + } + default: + throw new Exception("Unsupported data types for hexadecimal conversion"); + } + } + + private bool TypeTlmLenCheck(string type, int bitlen) + { + int typelen = 0; + typelen = type switch + { + "uint8_t" => sizeof(Byte), + "uint8" => sizeof(Byte), + + "int8_t" => sizeof(SByte), + "int8" => sizeof(SByte), + + "uint16_t" => sizeof(UInt16), + "uint16" => sizeof(UInt16), + + "int16_t" => sizeof(Int16), + "int16" => sizeof(Int16), + + "uint32_t" => sizeof(UInt32), + "uint32" => sizeof(UInt32), + + "int32_t" => sizeof(Int32), + "int32" => sizeof(Int32), + + "float" => sizeof(Single), + "double" => sizeof(Double), + + _ => 0 + }; + return (typelen * 8 == bitlen); + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Processor/Interfaces/ITcPacketGenerator.cs b/aspnetapp/WINGS/Services/TMTC/Processor/Interfaces/ITcPacketGenerator.cs new file mode 100644 index 0000000..3ba9552 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Processor/Interfaces/ITcPacketGenerator.cs @@ -0,0 +1,9 @@ +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ITcPacketGenerator + { + TcPacketData GetTcPacketData(string opid, Command command); + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Processor/Interfaces/ITmPacketAnalyzer.cs b/aspnetapp/WINGS/Services/TMTC/Processor/Interfaces/ITmPacketAnalyzer.cs new file mode 100644 index 0000000..934fea9 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Processor/Interfaces/ITmPacketAnalyzer.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ITmPacketAnalyzer + { + Task AnalyzePacketAsync(TmPacketData data, List prevTelemetry); + void RemoveOperation(string opid); + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/ISSL_COMMON/IsslCommonTcPacketGenerator.cs b/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/ISSL_COMMON/IsslCommonTcPacketGenerator.cs new file mode 100644 index 0000000..394714e --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/ISSL_COMMON/IsslCommonTcPacketGenerator.cs @@ -0,0 +1,71 @@ +using System; +using WINGS.Models; + +namespace WINGS.Services +{ + public class IsslCommonTcPacketGenerator : TcPacketGeneratorBase, ITcPacketGenerator + { + protected static readonly int TcPktCmmnHdrLen = 4; + protected static readonly int TcPktCmdHdrLen = 4; + protected static readonly int TcPktFtrLen = 4; + protected static Byte command_count; + + static IsslCommonTcPacketGenerator() + { + command_count = 0; + } + + protected override byte[] GeneratePacket(Command command) + { + int paramsLen = GetParamsByteLength(command); + var tcPktBdyLen = (UInt16)(TcPktCmdHdrLen + paramsLen); + var tcPktLen = (UInt16)(TcPktCmmnHdrLen + tcPktBdyLen + TcPktFtrLen); + var packet = new byte[tcPktLen]; + + // Header + // STX + packet[0] = 0xeb; + packet[1] = 0x90; + // Length + byte val = (byte)(tcPktBdyLen >> 8); + packet[2] = val; + val = (byte)(tcPktBdyLen & 0xff); + packet[3] = val; + + // Body + // VERSION_ID + packet[4] = 0x01; + // CMMAND_COUNT + command_count += 1; + packet[5] = command_count; + // COMMAND_ID + UInt16 id_tmp = UInt16.Parse(command.Code.Remove(0, 2), System.Globalization.NumberStyles.HexNumber); + val = (byte)(id_tmp >> 8); + packet[6] = val; + val = (byte)(id_tmp & 0xff); + packet[7] = val; + // ARGS + SetParams(packet, command.Params, TcPktCmmnHdrLen + TcPktCmdHdrLen); + + // Footer + // CRC + var crc = CalcCRC(packet); + packet[tcPktLen - TcPktFtrLen] = crc[0]; + packet[tcPktLen - TcPktFtrLen + 1] = crc[1]; + // ETX + packet[tcPktLen - TcPktFtrLen + 2] = 0xc5; + packet[tcPktLen - TcPktFtrLen + 3] = 0x79; + + return packet; + } + + protected virtual byte[] CalcCRC(byte[] packet) + { + // CRCはこのクラスでは実装しない、継承先で実装する + var crc = new byte[2]; + crc[0] = 0x00; + crc[1] = 0x00; + return crc; + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/ISSL_COMMON/IsslCommonTmPacketAnalyzer.cs b/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/ISSL_COMMON/IsslCommonTmPacketAnalyzer.cs new file mode 100644 index 0000000..b0570ad --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/ISSL_COMMON/IsslCommonTmPacketAnalyzer.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WINGS.Data; +using WINGS.Models; + +namespace WINGS.Services +{ + public class IsslCommonTmPacketAnalyzer : TmPacketAnalyzerBase, ITmPacketAnalyzer + { + public IsslCommonTmPacketAnalyzer(ITelemetryLogRepository logRepository) : base(logRepository) + { + } + + public override async Task AnalyzePacketAsync(TmPacketData data, List prevTelemetry) + { + var packetId = GetPacketId(data.TmPacket); + var isRealtimeData = true; + UInt32 TI = 0; + return await SetTelemetryValuesAsync(data, packetId, isRealtimeData, TI, prevTelemetry); + } + + private string GetPacketId(byte[] packet) + { + int pos = 6; + return string.Format("0x{0:x2}", packet[pos]); + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/MOBC/MobcTcPacketGenerator.cs b/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/MOBC/MobcTcPacketGenerator.cs new file mode 100644 index 0000000..26c6cbe --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/MOBC/MobcTcPacketGenerator.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections.Generic; +using WINGS.Data; +using WINGS.Models; +using WINGS.Library; + +namespace WINGS.Services +{ + public class MobcTcPacketGenerator : TcPacketGeneratorBase, ITcPacketGenerator + { + // static readonly int + // User Data + private static readonly int UserDataHdrLen = 8; + // TC Packet + private static readonly int TcPktPriHdrLen = 6; + private static readonly int TcPktSecHdrLen = 1; + // TC Segment + private static readonly int TcSgmHdrLen = 1; + // TC Transfer Frame + private static readonly int TcTrsFrmPriHdrLen = 5; + private static readonly int CrcLen = 2; + // pos + private static readonly int TcTrsFrmPos = 0; + private static readonly int TcSgmPos = TcTrsFrmPos + TcTrsFrmPriHdrLen; + private static readonly int TcPktPos = TcSgmPos + TcSgmHdrLen; + private static readonly int UserDataPos = TcPktPos + TcPktPriHdrLen + TcPktSecHdrLen; + + + // enum + // User Data + private enum UdCmdType { Dc = 1, Sm = 2 } + private enum UdExeType { Realtime = 0x00, Timeline = 0x01, Macro = 0x02, UnixTimeline = 0x04, MobcRealtime = 0x10, MobcTimeline = 0x11, MobcMacro = 0x12, MobcUnixTimeline = 0x14, AobcRealtime = 0x20, AobcTimeline = 0x21, AobcMacro = 0x22, AobcUnixTimeline = 0x24, TobcRealtime = 0x30, TobcTimeline = 0x31, TobcMacro = 0x32, TobcUnixTimeline = 0x34} + + // TC Packet + private enum TcpVer { Ver1 = 0 } + private enum TcpType { Tlm = 0, Cmd = 1 } + private enum TcpSecHdrFlag { Absent = 0, Present = 1 } + private enum TcpApid { MobcCmd = 0x210, AobcCmd = 0x211, TobcCmd = 0x212 } + private enum TcpSeqFlag { Cont = 0, First = 1, Last = 2, Single = 3 } + private enum TcpSeqCnt { Default = 0 } + private enum TcpFmtId { Control = 1, User = 2, Memory = 3 } + + // TC Segment + private enum TcsgmSeqFlag { First = 0b01, Continuing = 0b00, Last = 0b10, No = 0b11 } + private enum TcsgmMltAccPntId { DhuHrdDcd = 0b000001, Normal = 0b000010, Long = 0b000100 } + + // TC Transfer Frame + private enum TctfVer { Ver1 = 0b00 } + private enum TctfBypsFlag { TypeA = 0b0, TypeB = 0b1 } + private enum TctfCtlCmdFlag { Dmode = 0b0, Cmode = 0b1 } + private enum TctfSpare { Fixed = 0b00 } + private enum TctfScId { Default = 0x35C } + private enum TctfVirChId { Default = 0b000000} + private enum TctfSeq { TypeB = 0x00 } + + + + protected override byte[] GeneratePacket(Command command) + { + int paramsLen = GetParamsByteLength(command); + var tctfPktLen = (UInt16)(TcTrsFrmPriHdrLen + TcSgmHdrLen + TcPktPriHdrLen + TcPktSecHdrLen + UserDataHdrLen + paramsLen + CrcLen); + var packet = new byte[tctfPktLen]; + + var tcpPktLen = (UInt16)(TcPktSecHdrLen + UserDataHdrLen + paramsLen); // "PACKET FIELD" Length + var channelId = GetChannelId(command); + var exeType = GetExeType(command); + var apid = GetApid(command); + var ti = GetTi(command); + + //TC Transfer Frame (except CRC) + SetTctfVer(packet, TctfVer.Ver1); + SetTctfBypsFlag(packet, TctfBypsFlag.TypeB); + SetTctfCtlCmdFlag(packet, TctfCtlCmdFlag.Dmode); + SetTctfSpare(packet, TctfSpare.Fixed); + SetTctfScId(packet, TctfScId.Default); + SetTctfVirChId(packet, TctfVirChId.Default); + SetTctfLen(packet, tctfPktLen); + SetTctfSeq(packet, TctfSeq.TypeB); + + //TC Segment + SetTcsgmSeqFlag(packet, TcsgmSeqFlag.No); + SetTcsgmMltAccPntId(packet, TcsgmMltAccPntId.Normal); + + // TC Packet + SetTcpVerNum(packet, TcpVer.Ver1); + SetTcpType(packet, TcpType.Cmd); + SetTcpSecHdrFlag(packet, TcpSecHdrFlag.Present); + SetTcpApid(packet, apid); + SetTcpSeqFlag(packet, TcpSeqFlag.Single); + SetTcpSeqCnt(packet, TcpSeqCnt.Default); + SetTcpPktLen(packet, tcpPktLen); + SetTcpFmtId(packet, TcpFmtId.Control); + + // User Data + SetUdCmdType(packet, UdCmdType.Sm); + SetUdChannelId(packet, channelId); + SetUdExeType(packet, exeType); + SetUdTi(packet, ti, exeType); + SetParams(packet, command.Params, UserDataPos + UserDataHdrLen); + + //CRC + SetTctfCrc(packet, tctfPktLen); + + return packet; + } + + private UInt16 GetChannelId(Command command) + { + return UInt16.Parse(command.Code.Remove(0,2), System.Globalization.NumberStyles.HexNumber); + } + private UdExeType GetExeType(Command command) + { + if (command.IsViaMobc) + { + switch (command.ExecType) + { + case CmdExecType.RT: + return UdExeType.Realtime; + case CmdExecType.TL: + return UdExeType.Timeline; + case CmdExecType.BL: + return UdExeType.Macro; + case CmdExecType.UTL: + return UdExeType.UnixTimeline; + default: + return UdExeType.Realtime; + } + } + else + { + switch (command.Component) + { + case "MOBC": + switch (command.ExecType) + { + case CmdExecType.RT: + return UdExeType.Realtime; + case CmdExecType.TL: + return UdExeType.Timeline; + case CmdExecType.BL: + return UdExeType.Macro; + case CmdExecType.UTL: + return UdExeType.UnixTimeline; + default: + return UdExeType.Realtime; + } + case "AOBC": + switch (command.ExecType) + { + case CmdExecType.RT: + return UdExeType.AobcRealtime; + case CmdExecType.TL: + return UdExeType.AobcTimeline; + case CmdExecType.BL: + return UdExeType.AobcMacro; + case CmdExecType.UTL: + return UdExeType.UnixTimeline; + default: + return UdExeType.AobcRealtime; + } + case "TOBC": + switch (command.ExecType) + { + case CmdExecType.RT: + return UdExeType.TobcRealtime; + case CmdExecType.TL: + return UdExeType.TobcTimeline; + case CmdExecType.BL: + return UdExeType.TobcMacro; + case CmdExecType.UTL: + return UdExeType.UnixTimeline; + default: + return UdExeType.TobcRealtime; + } + default: + switch (command.ExecType) + { + case CmdExecType.RT: + return UdExeType.Realtime; + case CmdExecType.TL: + return UdExeType.Timeline; + case CmdExecType.BL: + return UdExeType.Macro; + case CmdExecType.UTL: + return UdExeType.UnixTimeline; + default: + return UdExeType.Realtime; + } + } + } + } + private TcpApid GetApid(Command command) + { + switch (command.Component) + { + case "MOBC": + return TcpApid.MobcCmd; + case "AOBC": + return TcpApid.AobcCmd; + case "TOBC": + return TcpApid.TobcCmd; + default: + return TcpApid.MobcCmd; + } + } + private uint GetTi(Command command) + { + return command.ExecTime; + } + + private void SetTctfVer(byte[] packet, TctfVer ver) + { + int pos = TcTrsFrmPos; + byte mask = 0b_1100_0000; + byte val = (byte)((byte)ver << 6); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + + private void SetTctfBypsFlag(byte[] packet, TctfBypsFlag flag) + { + int pos = TcTrsFrmPos; + byte mask = 0b_0010_0000; + byte val = (byte)((byte)flag << 5); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + + private void SetTctfCtlCmdFlag(byte[] packet, TctfCtlCmdFlag flag) + { + int pos = TcTrsFrmPos; + byte mask = 0b_0001_0000; + byte val = (byte)((byte)flag << 4); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + + private void SetTctfSpare(byte[] packet, TctfSpare spare) + { + int pos = TcTrsFrmPos; + byte mask = 0b_0000_1100; + byte val = (byte)((byte)spare << 2); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + + private void SetTctfScId(byte[] packet, TctfScId id) + { + int pos = TcTrsFrmPos; + byte mask = 0b_0000_0011; + byte val = (byte)((UInt16)id >> 8 & mask); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + val = (byte)((UInt16)id & 0xff); + packet[pos+1] = val; + } + + private void SetTctfVirChId(byte[] packet, TctfVirChId id) + { + int pos = TcTrsFrmPos + 2; + byte mask = 0b_1111_1100; + byte val = (byte)((byte)id << 2); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + + private void SetTctfLen(byte[] packet, UInt16 len) + { + int pos = TcTrsFrmPos + 2; + byte mask = 0b_0000_0011; + UInt16 z_origin = (UInt16)(len - 1); // Total octets in the TC Transfer Frame - 1 + byte val = (byte)(z_origin >> 8 & mask); + packet[pos] = val; + val = (byte)(z_origin & 0xff); + packet[pos+1] = val; + } + + private void SetTctfSeq(byte[] packet, TctfSeq seq) + { + int pos = TcTrsFrmPos + 4; + packet[pos] = (byte)seq; + } + + private void SetTctfCrc(byte[] packet, UInt16 tctfPktLen) + { + int pos = packet.Length - 2; + UInt16 crc_tmp = CRC.CRC16CCITTLeftCalc(packet[..^2], 0xffff); + byte val = (byte)(crc_tmp >> 8); + packet[pos] = val; + val = (byte)(crc_tmp & 0xff); + packet[pos + 1] = val; + } + + private void SetTcsgmSeqFlag(byte[] packet, TcsgmSeqFlag flag) + { + int pos = TcSgmPos; + byte mask = 0b_1100_0000; + byte val = (byte)((byte)flag << 6); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + private void SetTcsgmMltAccPntId(byte[] packet, TcsgmMltAccPntId id) + { + int pos = TcSgmPos; + byte mask = 0b_0011_1111; + packet[pos] &= (byte)(~mask); + packet[pos] |= (byte)id; + } + + private void SetTcpVerNum(byte[] packet, TcpVer ver) + { + int pos = TcPktPos; + byte mask = 0b_1110_0000; + byte val = (byte)((byte)ver << 5); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + private void SetTcpType(byte[] packet, TcpType type) + { + int pos = TcPktPos; + byte mask = 0b_0001_0000; + byte val = (byte)((byte)type << 4); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + private void SetTcpSecHdrFlag(byte[] packet, TcpSecHdrFlag flag) + { + int pos = TcPktPos; + byte mask = 0b_0000_1000; + byte val = (byte)((byte)flag << 3); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + private void SetTcpApid(byte[] packet, TcpApid apid) + { + int pos = TcPktPos; + byte mask = 0b_0000_0111; + byte val = (byte)((UInt16)apid >> 8 & mask); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + val = (byte)((UInt16)apid & 0xff); + packet[pos+1] = val; + } + private void SetTcpSeqFlag(byte[] packet, TcpSeqFlag flag) + { + int pos = TcPktPos + 2; + byte mask = 0b_1100_0000; + byte val = (byte)((byte)flag << 6); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + } + private void SetTcpSeqCnt(byte[] packet, TcpSeqCnt cnt) + { + int pos = TcPktPos + 2; + byte mask = 0b_0011_1111; + byte val = (byte)((UInt16)cnt >> 8 & mask); + packet[pos] &= (byte)(~mask); + packet[pos] |= val; + val = (byte)((UInt16)cnt & 0xff); + packet[pos+1] = val; + } + private void SetTcpPktLen(byte[] packet, UInt16 len) + { + int pos = TcPktPos + 4; + UInt16 z_origin = (UInt16)(len - 1); // TCPacketのLengthは0起算表記なので1起算に変換 + byte val = (byte)(z_origin >> 8); + packet[pos] = val; + val = (byte)(z_origin & 0xff); + packet[pos+1] = val; + } + private void SetTcpFmtId(byte[] packet, TcpFmtId id) + { + int pos = TcPktPos + 6; + packet[pos] = (byte)id; + } + private void SetUdCmdType(byte[] packet, UdCmdType type) + { + int pos = UserDataPos; + packet[pos] = (byte)type; + } + private void SetUdChannelId(byte[] packet, UInt16 id) + { + int pos = UserDataPos + 1; + byte val = (byte)(id >> 8); + packet[pos] = val; + val = (byte)(id & 0xff); + packet[pos+1] = val; + } + private void SetUdExeType(byte[] packet, UdExeType type) + { + int pos = UserDataPos + 3; + packet[pos] = (byte)type; + } + private void SetUdTi(byte[] packet, uint ti, UdExeType type) + { + int pos = UserDataPos + 4; + if(type == UdExeType.UnixTimeline || type == UdExeType.AobcUnixTimeline || type == UdExeType.TobcUnixTimeline) + { + uint epoch_unix_time = 1577836800; // UNIX TIME of 2020/1/1 00:00:00(UTC) + uint converted_unix_time = 0; + if (ti - epoch_unix_time > 0) + { + converted_unix_time = (ti - epoch_unix_time)*10; + } + byte val = (byte)(converted_unix_time >> 24); + packet[pos] = val; + val = (byte)(converted_unix_time >> 16 & 0xff); + packet[pos + 1] = val; + val = (byte)(converted_unix_time >> 8 & 0xff); + packet[pos + 2] = val; + val = (byte)(converted_unix_time & 0xff); + packet[pos + 3] = val; + } + else + { + byte val = (byte)(ti >> 24); + packet[pos] = val; + val = (byte)(ti >> 16 & 0xff); + packet[pos+1] = val; + val = (byte)(ti >> 8 & 0xff); + packet[pos+2] = val; + val = (byte)(ti & 0xff); + packet[pos+3] = val; + } + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/MOBC/MobcTmPacketAnalyzer.cs b/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/MOBC/MobcTmPacketAnalyzer.cs new file mode 100644 index 0000000..8ab0d14 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Processor/UserDefined/MOBC/MobcTmPacketAnalyzer.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WINGS.Data; +using WINGS.Models; + +namespace WINGS.Services +{ + + public class MobcTmPacketAnalyzer : TmPacketAnalyzerBase, ITmPacketAnalyzer + { + private static Dictionary> statictmPacket= new Dictionary>(); + + public MobcTmPacketAnalyzer(ITelemetryLogRepository logRepository) : base(logRepository) + { + } + + //STX + private enum Ver { Ver1 = 0b00, Ver2 = 0b01 } + private enum ScId { Issl6U = 0x000 } + private enum VirChId { Realtime = 0b000001, Replay = 0b000010, Fill = 0b111111 } + + //ETX + private enum CtlWrdType { CLCW = 0b0 } + private enum ClcwVer { Ver1 = 0b00 } + private enum COPinEff { COP1 = 0b01 } + private enum VCId { Default = 0b000000 } + private enum Spare { Fixed = 0b00 } + + public override async Task AnalyzePacketAsync(TmPacketData data, List prevTelemetry) + { + //data.TmPacket : Transferframe + //not support ADU-split + const int tfPriHeaderLen = 6; + const int mpduPriHeaderLen = 2; + const int mpduPacketZone = 432; + const int ccsdsPriHeaderLen = 6; + const int ccsdsSecHeaderLen = 7; //ADU-split -> 12 + const int headerLength = 6; + const int bodyLength = 434; + const int footerLength = 4; + const int transferFrameLength = headerLength + bodyLength + footerLength; + const int footerIndex = transferFrameLength - footerLength; + + UInt16 userdataLen = 0; //ccsdsSecHeaderLen + ADULen + byte[] ccsdsTmPacketbuf = default; + var ccsdsdataList = new List(); + var packetIdList = new List(); + var realtimeFlagList = new List(); + var TIList = new List(); + + + if (data.TmPacket.Length == 512) + { + data.TmPacket = data.TmPacket[4..448]; + } + + var STX = data.TmPacket[0..headerLength]; + if (AnalyzeHeader(STX)) + { + var ETX = data.TmPacket[footerIndex..transferFrameLength]; + if (AnalyzeFooter(ETX)) + { + if (statictmPacket.Keys.Contains(data.Opid) == false) + { + statictmPacket.Add(data.Opid, new List()); + } + var ccsdsPriHeaderPointer = CombiteBytes(data.TmPacket, tfPriHeaderLen) & 0b_0000_0111_1111_1111; + if (ccsdsPriHeaderPointer == 0b_0111_1111_1111) + { + //No CCSDS Packet starts in this Transferframe + for (uint i = tfPriHeaderLen + mpduPriHeaderLen; i < tfPriHeaderLen + mpduPriHeaderLen + mpduPacketZone; i++) + { + statictmPacket[data.Opid].Add(data.TmPacket[i]); + } + ccsdsTmPacketbuf = statictmPacket[data.Opid].ToArray(); + userdataLen = CombiteBytesGetLen(ccsdsTmPacketbuf, 4); + if (ccsdsTmPacketbuf.Length == ccsdsPriHeaderLen + userdataLen) + { + AddTelemetryList(data.Opid, ccsdsTmPacketbuf, ccsdsdataList, packetIdList, realtimeFlagList, TIList); + statictmPacket[data.Opid].Clear(); + } + } + else if(ccsdsPriHeaderPointer == 0b_0111_1111_1110) + { + //What is "idle data" and how to process it? + throw new Exception("Idle data is not supported."); + } + else + { + //"firstHeaderPointer" == 0 <=> M_PDU Packet Zone starts with the head of the first CCSDS Packet. + if (ccsdsPriHeaderPointer != 0) + { + for (int i = tfPriHeaderLen + mpduPriHeaderLen; i < tfPriHeaderLen + mpduPriHeaderLen + ccsdsPriHeaderPointer; i++) + { + statictmPacket[data.Opid].Add(data.TmPacket[i]); + } + ccsdsTmPacketbuf = statictmPacket[data.Opid].ToArray(); + userdataLen = CombiteBytesGetLen(ccsdsTmPacketbuf, 4); + if (ccsdsTmPacketbuf.Length == ccsdsPriHeaderLen + userdataLen) + { + AddTelemetryList(data.Opid, ccsdsTmPacketbuf, ccsdsdataList, packetIdList, realtimeFlagList, TIList); + statictmPacket[data.Opid].Clear(); + } + else + { + //error : Queue data is incorrect. + //throw new Exception("Queue data is incorrect."); + statictmPacket[data.Opid].Clear(); + } + } + + var readPointer = tfPriHeaderLen + mpduPriHeaderLen + ccsdsPriHeaderPointer; + while (readPointer != tfPriHeaderLen + mpduPriHeaderLen + mpduPacketZone) + { + if (tfPriHeaderLen + mpduPriHeaderLen + mpduPacketZone - readPointer < ccsdsPriHeaderLen + ccsdsSecHeaderLen) + { + for (int i = readPointer; i < tfPriHeaderLen + mpduPriHeaderLen + mpduPacketZone; i++) + { + statictmPacket[data.Opid].Add(data.TmPacket[i]); + } + break; + } + else + { + userdataLen = CombiteBytesGetLen(data.TmPacket, readPointer + 4); + if (tfPriHeaderLen + mpduPriHeaderLen + mpduPacketZone - readPointer < ccsdsPriHeaderLen + userdataLen) + { + for (int i = readPointer; i < tfPriHeaderLen + mpduPriHeaderLen + mpduPacketZone; i++) + { + statictmPacket[data.Opid].Add(data.TmPacket[i]); + } + break; + } + else + { + if (statictmPacket[data.Opid].Count != 0) + { + //error : Queue data should be empty here. + //throw new Exception("Queue data should be empty here."); + statictmPacket[data.Opid].Clear(); + } + for (int i = readPointer; i < readPointer + ccsdsPriHeaderLen + userdataLen; i++) + { + statictmPacket[data.Opid].Add(data.TmPacket[i]); + } + AddTelemetryList(data.Opid, statictmPacket[data.Opid].ToArray(), ccsdsdataList, packetIdList, realtimeFlagList, TIList); + statictmPacket[data.Opid].Clear(); + readPointer += ccsdsPriHeaderLen + userdataLen; + } + } + } + } + } + } + return await SetTelemetryListValuesAsync(ccsdsdataList, packetIdList, realtimeFlagList, TIList, prevTelemetry); + } + + private void AddTelemetryList(string opid, byte[] ccsdstmPacket, List ccsdsdataList, List packetIdList, List realtimeFlagList, List TIList) + { + ccsdsdataList.Add(new TmPacketData{ Opid = opid, TmPacket = ccsdstmPacket }); + packetIdList.Add(GetPacketId(ccsdstmPacket)); + realtimeFlagList.Add(GetRealTimeFlag(ccsdstmPacket)); + TIList.Add(GetTI(ccsdstmPacket)); + } + + private UInt16 CombiteBytes(byte[] bytes, int pos) + { + UInt16 byte1 = (UInt16)bytes[pos]; + UInt16 byte2 = (UInt16)bytes[pos + 1]; + UInt16 byte1s = (UInt16)(byte1 << 8); + return (UInt16)(byte1s + byte2); + } + private UInt16 CombiteBytesGetLen(byte[] bytes, int pos) + { + UInt16 byte1 = (UInt16)bytes[pos]; + UInt16 byte2 = (UInt16)bytes[pos + 1]; + UInt16 byte1s = (UInt16)(byte1 << 8); + return (UInt16)(byte1s + byte2 + 1); //起算 + } + + private string GetPacketId(byte[] packet) + { + //packet : CCSDS Packet + int pos = 11; + return string.Format("0x{0:x2}", packet[pos]); + } + private bool GetRealTimeFlag(byte[] packet) + { + //packet : CCSDS Packet + int pos = 10; + if ((packet[pos] & 0b_1110_0000) == 0b_0000_0000) // rp + { + return false; + } + else // HK or MS( or stored) + { + return true; + } + } + private UInt32 GetTI(byte[] packet) + { + //packet : CCSDS Packet + int pos = 6; + UInt32 byte1 = (UInt32)(packet[pos] << 24); + UInt32 byte2 = (UInt32)(packet[pos + 1] << 16); + UInt32 byte3 = (UInt32)(packet[pos + 2] << 8); + UInt32 byte4 = (UInt32)(packet[pos + 3]); + return (UInt32)(byte1 + byte2 + byte3 + byte4); + } + + protected bool AnalyzeHeader(byte[] STX) + { + //only use fixed bits + if (!ChkVer(STX, Ver.Ver2)) { return false; } + return true; + } + + protected bool AnalyzeFooter(byte[] ETX) + { + //only use fixed bits + if (!ChkCtlWrdType(ETX, CtlWrdType.CLCW)) { return false; } + if (!ChkClcwVer(ETX, ClcwVer.Ver1)) { return false; } + if (!ChkCOPinEff(ETX, COPinEff.COP1)) { return false; } + if (!ChkVCId(ETX, VCId.Default)) { return false; } + if (!ChkSpare(ETX, Spare.Fixed)) { return false; } + return true; + } + + private bool ChkVer(byte[] STX, Ver ver) + { + int pos = 0; + byte mask = 0b_1100_0000; + byte val = (byte)((byte)ver << 6); + return (STX[pos] & mask) == val; + } + + private bool ChkScId(byte[] STX, ScId id) + { + int pos1 = 0; + byte mask1 = 0b_0011_1111; + byte val1 = (byte)((byte)id >> 2); + int pos2 = 1; + byte mask2 = 0b_1100_0000; + byte val2 = (byte)((byte)id << 6); + return ((STX[pos1] & mask1) == val1) & ((STX[pos2] & mask2) == val2); + } + + private bool ChkCtlWrdType(byte[] ETX, CtlWrdType type) + { + int pos = 0; + byte mask = 0b_1000_0000; + byte val = (byte)((byte)type << 7); + return (ETX[pos] & mask) == val; + } + + private bool ChkClcwVer(byte[] ETX, ClcwVer ver) + { + int pos = 0; + byte mask = 0b_0110_0000; + byte val = (byte)((byte)ver << 5); + return (ETX[pos] & mask) == val; + } + + private bool ChkCOPinEff(byte[] ETX, COPinEff eff) + { + int pos = 0; + byte mask = 0b_0000_0011; + byte val = (byte)((byte)eff); + return (ETX[pos] & mask) == val; + } + + private bool ChkVCId(byte[] ETX, VCId id) + { + int pos = 1; + byte mask = 0b_1111_1100; + byte val = (byte)((byte)id << 2); + return (ETX[pos] & mask) == val; + } + + private bool ChkSpare(byte[] ETX, Spare spare) + { + int pos = 1; + byte mask = 0b_0000_0011; + byte val = (byte)((byte)spare); + return (ETX[pos] & mask) == val; + } + + + + public override void RemoveOperation(string opid) + { + statictmPacket.Remove(opid); + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Transferer/Interfaces/ITmtcPacketService.cs b/aspnetapp/WINGS/Services/TMTC/Transferer/Interfaces/ITmtcPacketService.cs new file mode 100644 index 0000000..6dad4a7 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Transferer/Interfaces/ITmtcPacketService.cs @@ -0,0 +1,9 @@ +using WINGS.Models; + +namespace WINGS.Services +{ + public interface ITmtcPacketService + { + void Send(TcPacketData data); + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/ITcPacketQueue.cs b/aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/ITcPacketQueue.cs new file mode 100644 index 0000000..12eb1c7 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/ITcPacketQueue.cs @@ -0,0 +1,13 @@ +using WINGS.Models; + +namespace WINGS.Services.TmtcIf +{ + public interface ITcPacketQueue + { + void Add(string opid); + void Remove(string opid); + void Enqueue(TcPacketData data); + TcPacketData Dequeue(string opid); + bool PacketQueueExists(string opid); + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/TcPacketQueue.cs b/aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/TcPacketQueue.cs new file mode 100644 index 0000000..785f801 --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/TcPacketQueue.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WINGS.Models; + +namespace WINGS.Services.TmtcIf +{ + public class TcPacketQueue : ITcPacketQueue + { + private Dictionary> _packetQueueDict; + + public TcPacketQueue() + { + _packetQueueDict = new Dictionary>(); + } + + public void Add(string opid) + { + _packetQueueDict.Add(opid, new Queue()); + } + + public void Remove(string opid) + { + _packetQueueDict.Remove(opid); + } + + public void Enqueue(TcPacketData data) + { + if (!_packetQueueDict.TryGetValue(data.Opid, out var packetQueue)) + { + throw new ResourceNotFoundException("The packet queue is not found"); + } + packetQueue.Enqueue(data); + } + + public TcPacketData Dequeue(string opid) + { + if (!_packetQueueDict.TryGetValue(opid, out var packetQueue)) + { + throw new ResourceNotFoundException("The packet queue is not found"); + } + return packetQueue.Dequeue(); + } + + public bool PacketQueueExists(string opid) + { + if (!_packetQueueDict.TryGetValue(opid, out var packetQueue)) + { + throw new ResourceNotFoundException("The packet queue is not found"); + } + return packetQueue.Count != 0; + } + } +} diff --git a/aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/TmtcPacketService.cs b/aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/TmtcPacketService.cs new file mode 100644 index 0000000..46ac97c --- /dev/null +++ b/aspnetapp/WINGS/Services/TMTC/Transferer/TmtcIf/TmtcPacketService.cs @@ -0,0 +1,100 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Grpc.Core; +using Google.Protobuf; +using WINGS.Data; +using WINGS.Models; +using WINGS.GrpcService; + +namespace WINGS.Services.TmtcIf +{ + public class TmtcPacketService : TmtcPacket.TmtcPacketBase, ITmtcPacketService + { + private readonly ApplicationDbContext _dbContext; + private readonly ITmPacketManager _tmPacketManager; + private readonly ITcPacketQueue _tcPacketQueue; + + public TmtcPacketService(ApplicationDbContext dbContext, + ITmPacketManager tmPacketManager, + ITcPacketQueue tcPacketQueue) + { + _dbContext = dbContext; + _tmPacketManager = tmPacketManager; + _tcPacketQueue = tcPacketQueue; + } + + public override async Task TmPacketTransfer(TmPacketDataRpc dataRpc, ServerCallContext context) + { + var data = FromRpcModel(dataRpc); + bool ack = true; + try + { + await _tmPacketManager.RegisterTelemetryAsync(data); + } + catch (ResourceNotFoundException) + { + ack = false; + } + return (new TmPacketResponseRpc + { + Opid = data.Opid, + Ack = ack + }); + } + + public override async Task TcPacketTransfer(TcPacketRequestRpc request, IServerStreamWriter responseStream, ServerCallContext context) + { + var contextCancellationToken = context.CancellationToken; + string opid = request.Opid; + + _tcPacketQueue.Add(opid); + await SetTmtcClientStatusAsync(opid, true); + + while (true) + { + if (contextCancellationToken.IsCancellationRequested) + { + _tcPacketQueue.Remove(opid); + await SetTmtcClientStatusAsync(opid, false); + return; + } + + if (_tcPacketQueue.PacketQueueExists(opid)) + { + var dataRpc = ToRpcModel(_tcPacketQueue.Dequeue(opid)); + await responseStream.WriteAsync(dataRpc); + } + await Task.Delay(100); + } + } + + public void Send(TcPacketData data) + { + _tcPacketQueue.Enqueue(data); + } + + private async Task SetTmtcClientStatusAsync(string opid, bool isConnected) + { + var operation = await _dbContext.Operations.FindAsync(opid); + operation.IsTmtcConnected = isConnected; + _dbContext.Entry(operation).State = EntityState.Modified; + await _dbContext.SaveChangesAsync(); + } + + private TmPacketData FromRpcModel(TmPacketDataRpc dataRpc) + { + return new TmPacketData{ + Opid = dataRpc.Opid, + TmPacket = dataRpc.TmPacket.ToByteArray() + }; + } + + private TcPacketDataRpc ToRpcModel(TcPacketData data) + { + return new TcPacketDataRpc{ + Opid = data.Opid, + TcPacket = ByteString.CopyFrom(data.TcPacket) + }; + } + } +} diff --git a/aspnetapp/WINGS/Startup.cs b/aspnetapp/WINGS/Startup.cs new file mode 100644 index 0000000..dab7900 --- /dev/null +++ b/aspnetapp/WINGS/Startup.cs @@ -0,0 +1,123 @@ +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; +using Microsoft.EntityFrameworkCore; +using WINGS.Data; +using WINGS.Models; +using WINGS.Services; +using WINGS.Services.TmtcIf; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace WINGS +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(options => + options.UseMySql( + Configuration.GetConnectionString("DefaultConnection"),ServerVersion.Parse("8.0"))); + + services.AddControllersWithViews() + .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); + services.AddRazorPages(); + + services.AddGrpc(); + + services.AddDatabaseDeveloperPageExceptionFilter(); + + // Services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // TmtcIf + services.AddSingleton(); + services.AddTransient(); + + // Repositories + services.AddScoped(); + services.AddScoped, CommandDbRepository>(); + services.AddScoped, TelemetryDbRepository>(); + services.AddScoped, LayoutRepository>(); + services.AddScoped(); + services.AddScoped(); + + ConfigureUserDefinedServices(services); + + // In production, the React files will be served from this directory + services.AddSpaStaticFiles(configuration => + { + configuration.RootPath = "ClientApp/build"; + }); + } + + private void ConfigureUserDefinedServices(IServiceCollection services) + { + // MOBC + services.AddTransient(); + services.AddTransient(); + + //ISSL_COMMON + services.AddTransient(); + services.AddTransient(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseMigrationsEndPoint(); + } + else + { + app.UseExceptionHandler("/Error"); + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseSpaStaticFiles(); + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller}/{action=Index}/{id?}"); + endpoints.MapRazorPages(); + }); + + app.UseSpa(spa => + { + spa.Options.SourcePath = "ClientApp"; + + if (env.IsDevelopment()) + { + spa.UseReactDevelopmentServer(npmScript: "start"); + } + }); + } + } +} diff --git a/aspnetapp/WINGS/TlmCmd/ISSL_COMMON/cmddb/CMD_DB.csv b/aspnetapp/WINGS/TlmCmd/ISSL_COMMON/cmddb/CMD_DB.csv new file mode 100644 index 0000000..c47cdfb --- /dev/null +++ b/aspnetapp/WINGS/TlmCmd/ISSL_COMMON/cmddb/CMD_DB.csv @@ -0,0 +1,6 @@ +Comment,Name,Target,Code,Params,,,,,,,,,,,,,Danger Flag,Is Restricted,Description,Note +,,,,Num Params,Param1,,Param2,,Param3,,Param4,,Param5,,Param6,,,,, +,,,,,Type,Description,Type,Description,Type,Description,Type,Description,Type,Description,Type,Description,,,, +*,Cmd_EXAMPLE,OBC,,2,uint32_t,address,int32_t,time [ms],,,,,,,,,,,Example,Write description +* MIF,,,,,,,,,,,,,,,,,,,, +,Cmd_NOP,MIF,0x0000,0,,,,,,,,,,,,,,,Dummy, diff --git a/aspnetapp/WINGS/TlmCmd/ISSL_COMMON/tlmdb/HK.csv b/aspnetapp/WINGS/TlmCmd/ISSL_COMMON/tlmdb/HK.csv new file mode 100644 index 0000000..82d8681 --- /dev/null +++ b/aspnetapp/WINGS/TlmCmd/ISSL_COMMON/tlmdb/HK.csv @@ -0,0 +1,9 @@ +,Target,OBC,,,,,,,,,,,,,,, +,PacketID,0x00,,,,,,,,,,,,,,, +,Enable/Disable,ENABLE,,,,,,,,,,,,,,, +,IsRestricted,FALSE,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,, +Comment,TLM Entry,Onboard Software Info.,,Extraction Info.,,,,Conversion Info.,,,,,,,,Description,Note +,Name,Var. Type,Variable or Function Name,Ext. Type,Pos. Desiginator,,,Conv. Type,Poly (Σa_i * x^i),,,,,,Status,, +,,,,,Octet Pos.,bit Pos.,bit Len.,,a0,a1,a2,a3,a4,a5,,, +,cmd_counter,uint8,,PACKET,8,0,8,NONE,,,,,,,,command counter, diff --git a/aspnetapp/WINGS/TlmCmd/OBC/cmddb/.gitkeep b/aspnetapp/WINGS/TlmCmd/OBC/cmddb/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/aspnetapp/WINGS/TlmCmd/OBC/cmdplan/main/.gitkeep b/aspnetapp/WINGS/TlmCmd/OBC/cmdplan/main/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/aspnetapp/WINGS/TlmCmd/OBC/cmdplan/sub/.gitkeep b/aspnetapp/WINGS/TlmCmd/OBC/cmdplan/sub/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/aspnetapp/WINGS/TlmCmd/OBC/cmdplan/test/.gitkeep b/aspnetapp/WINGS/TlmCmd/OBC/cmdplan/test/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/aspnetapp/WINGS/TlmCmd/OBC/tlmdb/.gitkeep b/aspnetapp/WINGS/TlmCmd/OBC/tlmdb/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/aspnetapp/WINGS/TlmCmd/Sample/cmddb/CMD_DB.csv b/aspnetapp/WINGS/TlmCmd/Sample/cmddb/CMD_DB.csv new file mode 100644 index 0000000..66d4aa6 --- /dev/null +++ b/aspnetapp/WINGS/TlmCmd/Sample/cmddb/CMD_DB.csv @@ -0,0 +1,6 @@ +Comment,Name,Target,Code,Params,,,,,,,,,,,,,Danger Flag,Is Restricted,Description,Note +,,,,Num Params,Param1,,Param2,,Param3,,Param4,,Param5,,Param6,,,,, +,,,,,Type,Description,Type,Description,Type,Description,Type,Description,Type,Description,Type,Description,,,, +,,,,,,,,,,,,,,,,,,,, +,Cmd_NOP,SAMPLE,,0,,,,,,,,,,,,,,,Dummy Command, +,Cmd_POWER_ON,SAMPLE,,1,uint8_t,Component Number,,,,,,,,,,,,,Switch ON, diff --git a/aspnetapp/WINGS/TlmCmd/Sample/cmdplan/sample.ops b/aspnetapp/WINGS/TlmCmd/Sample/cmdplan/sample.ops new file mode 100644 index 0000000..02b6ef6 --- /dev/null +++ b/aspnetapp/WINGS/TlmCmd/Sample/cmdplan/sample.ops @@ -0,0 +1,54 @@ + #==============================================# + #Comment Out + #==============================================# + #Comand 1 + OBC_TL.Cmd_ValveOpen 1 2 #Inline Commnet + + #Component is "OBC" + #Exec Type is "TL" + #Name is "Cmd_ValveOpen" + #The first parameter is TL Exec Time + #Following parameters are command parameters + + #==============================================# + #Command 2 + OBC_RT.Cmd_ValveOpen 1 + + #Component is "OBC" + #Exec Type is "RT" + #Name is "Cmd_ValveOpen" + #Following parameters are command parameters + + #==============================================# + #Command 3(raw parameters) +.OBC_RT.Cmd_MEM_LOAD 0x12345678 0x123456781234567801 + + #Raw parameters are defined in command DB and must be the last parameter. + #"0x123456781234567801" is a raw parameter. + #raw parameter must be written in hex-style and within 64 bytes. + + #raw parameter can be devided. +.OBC_RT.Cmd_MEM_LOAD 0x12345678 0x12345678 0x12345678 0x01 +.OBC_RT.Cmd_MEM_LOAD 0x12345678 0x1234 0x5678 0x12345678 0x01 +.OBC_RT.Cmd_MEM_LOAD 0x12345678 0x123456781234567801 + #==============================================# + #StopFlag +.OBC_RT.Cmd_ValveOpen 1 +.#top "." is a stop flag. + + #==============================================# + #call + call test2.ops + #open test2.ops in a new tab. + + #==============================================# + #wait_sec + wait_sec 1.5 + #wait seconds + + #==============================================# + #check_value + check_value HK.OBC_MM_STS == FINISHED + #telemetry value is checked + #The operators are ==, <=, >=, <, >, != + #==============================================# \ No newline at end of file diff --git a/aspnetapp/WINGS/TlmCmd/Sample/tlmdb/HK.csv b/aspnetapp/WINGS/TlmCmd/Sample/tlmdb/HK.csv new file mode 100644 index 0000000..93417f9 --- /dev/null +++ b/aspnetapp/WINGS/TlmCmd/Sample/tlmdb/HK.csv @@ -0,0 +1,10 @@ +,Target,SAMPLE,,,,,,,,,,,,,,, +,PacketID,0x01,,,,,,,,,,,,,,, +,Enable/Disable,ENABLE,,,,,,,,,,,,,,, +,IsRestricted,FALSE,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,, +Comment,TLM Entry,Onboard Software Info.,,Extraction Info.,,,,Conversion Info.,,,,,,,,Description,Note +,Name,Var. Type,Variable or Function Name,Ext. Type,Pos. Desiginator,,,Conv. Type,Poly (a_i * x^i),,,,,,Status,, +,,,,,Octet Pos.,bit Pos.,bit Len.,,a0,a1,a2,a3,a4,a5,,, +,TI,uint32_t,,PACKET,6,0,32,NONE,,,,,,,,Packet Generation Time, +,MODE,uint8_t,,PACKET,10,0,8,STATUS,,,,,,,"0=init, 1=normal",Modes, diff --git a/aspnetapp/WINGS/WINGS.csproj b/aspnetapp/WINGS/WINGS.csproj new file mode 100644 index 0000000..79015c3 --- /dev/null +++ b/aspnetapp/WINGS/WINGS.csproj @@ -0,0 +1,64 @@ + + + net6.0 + true + Latest + false + ClientApp\ + $(DefaultItemExcludes);$(SpaRoot)node_modules\** + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %(DistFiles.Identity) + PreserveNewest + true + + + + \ No newline at end of file diff --git a/aspnetapp/WINGS/appsettings.Development.json b/aspnetapp/WINGS/appsettings.Development.json new file mode 100644 index 0000000..e98fcad --- /dev/null +++ b/aspnetapp/WINGS/appsettings.Development.json @@ -0,0 +1,13 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "server=localhost; uid=root; pwd=P@ssw0rd!; database=wings;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetapp/WINGS/appsettings.Production.json b/aspnetapp/WINGS/appsettings.Production.json new file mode 100644 index 0000000..5392044 --- /dev/null +++ b/aspnetapp/WINGS/appsettings.Production.json @@ -0,0 +1,25 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "server=localhost; uid=root; pwd=P@ssw0rd!; database=wings;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "HttpsInlineCertFile": { + "Url": "https://0.0.0.0:5001", + "Protocols": "Http2", + "Certificate": { + "Path": "certificate.pfx", + "Password": "P@ssw0rd!" + } + } + } + } +} diff --git a/aspnetapp/WINGS/certificate.pfx b/aspnetapp/WINGS/certificate.pfx new file mode 100644 index 0000000000000000000000000000000000000000..9339900ad054aa5242838de2aeaa730fe2621168 GIT binary patch literal 2638 zcmZWpXH*l|5>7}6By>X!y+}zO5RfiKT&g8>Wa&ti-WSABBGL^dQUs)nAS=BD5D^d% zLodrxCA7zabOj`om-X!1J@348&)k{$zWHYEpF3zA>=q3WgvP-j5GXRvAnuq6NCzy! zL65;W=ph;h-9_VQPyUCZ{R51nU8BNesx3em{!%f*fHXxo@DUmZK0xz9VE=`w&6&aU zn;9A|OofRbXlUq0!8q`!Rbw)<{C$fM*%H09_Z~9rNEU%&*|>GsHTc!kPtzF>cTulnoLiyjBeG3hCyu*lpj{jsANbM`V%P+%GJOz zQghmlu-*N2Fv-_qyXdUTH)Pa`E|`9rP`sXlix`b@F}i=N&=9#4UGMeO=^eys8A?vr zc~!RB%xIzy8Jvu6uIRL}s~&7%w7GcO9A4wQI5KQbcI?Td4+={2XilS?zd!jzNT1PX z!&c^w!-?HP%>!{!*95N2?ABHwJ9TT&+dOlxQs`DqMNAUFurd^_sPBtlj4`Xv=1IRN z(gUSCDty4GSIji1z5bs0dU~c5P41KTeOWe@ER&8>4&*umD9y@EPeyYq-TbR>j&|@# ztz2>o?oj$JrBTi1ZV_ztXCKs~961O6tNRiTci`~#CR}Mw`OeOqOm$sPE3uJX?m#2+ z*5a3MA4p7_Hmitp)FEUhPTl+blG!yBA3A&*z$qxyj){CV{^+>?-2H_DQgN411 z>RCHiK&s+u{KI^j|E9&%3eGS!On%3VWg)6LH&j>;l6oxe4Rb7CpZlK!dpB-XREPLey!<~)!B^iW*^5tyGqo?mP8OQ7VdSQv-AjpS09~J?t@hKku#m%il#$UF)dnp;Wm%F@q6- z70Kq6fuSH~>Xmpuf3X%PPC6ehzNkpWmF2E`-$N8f@R?*uP};=#vuhN2A_zkk6>EFW z@A*on)L>3|-#Rq!=*ZnIUp+#U1D@_xek_a*skMGJiu^`&1~~-;S&!}TC%*lBBWeRV zS|#kKhd;Xjw|*7tT3{y-mF`*I=5@dHGK6k1d>9GqimAgfl($<7)QSCm zhmOZHQd>BM0+Og5SsfTFOOPARQ;Rph6vT1 z5mai|0!g7f0Vg#i+M!-Yqu%j9s?Yjbbv{_DH?XaS53j}Cm(QD@(T7fE$9pg^Pc|bE zJwyAjp{`)Plex*Hy%l7^nvaOH)=->wR!@3ms&J#`D%46Rs!pC-t?*}i6a)wc;*{BdKib~~JfM<+06#zw)!eA# zgaBrLMZZvmfzDZ+u4sL(tO z5I}WP|1iM+U-%38 zagIok`{}N%U2NIP#rDC?^vQ1@$893uPxB{XdZ}4@ek&~VsKAN1pbZfBP~dSv*miYs zKd4Ob0{DS{1mzZ=N-9t1E8a>-iRRRj!aC)*l7q=Ggd295GN!7ftW8bMV zB>!>N8d$ZBsL8F{`)g~3{u1}+X`c8b_d<61>?!l2+2Qv!k_LemHjx9RE6G|Y6YGE! z<{kc_l4mKmDfCJgwgrYfliGN?uv?-d6=Np!7!$FajrL*9qKM^G;evUuYd2iv8vFf6 zO-s@wC-EFQtS!qdO0l{7w4FvNHx>@X)9;xAo@(}e?K2bLAT{eQ25qu&7n=oQie?lG z_EjGZq-34F9(MUAz3%r#D)DO3BUtJ~vlPirKaEed_n8Lydf?34Qf*_)qc>T@*}b^J zj71Gtr^M$C=R`f@o`BMvheRM*Sivh>dZ`MRD@oxr1HjsSL_|!`z8`<=*D3+d{9lx> z94*2C-SBmkEej9Z9rmXsRR*%6{D}^w+?W2AY%aD9Gud4qbxgFe?@Q$m-bkH~4Fc&U z8%kw;@-Y?-`YLphqp;npUy-R}t2H*F#@OnqX;0LtV<7sO#fi8m9ErPc)o}iepLH=0 z)PN{^lv92={O+-U-Vwx z(-BiivI9blc<#^^)3IBW#S-Rwj0)yCxl1rL4hxR~@1jp>#aEKKCe8?}^{-2olpZ+Z z4BSWZ$%9W)`|h!Ke6){|XoNOn5j6422Y3Pd9B?yT>_|Uv-**w7EwMdCmXqf)4=T|P zEOunl7gCEw9HX+go-~5DwuxUNJ+3|}<%M=~qdoy094Fu1#FsiNa@_y>%Zwm7*31l^ zf|`dkOCW8PH{H?RWsb#Ghjc-vKU)zRfywR|Cz`ub&R&%(ni^Q%KN8|IA{nDJSiz{) z3zc2k*@z@TzegF%*CZ5L9XC7Duj`bYj#J9$7bX_>k>;Jbc@I)t)$L^HEu5}OllGx6 zBe(v&Q|`U`k^sLkxEw5OcGl<0Lbws*ioT8(MAJcNQOqD31ekr`$dCwp!7b5>j%X~? kcTXbEI#h$%eWfAS%R2n0PB|(iIc0mFoN}{0{PUy#2c6WhjsO4v literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..62c29a5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: "3" +services: + aspnetapp: + image: wings-aspnetapp-img + build: ./aspnetapp + container_name: wings-aspnetapp-ctr + volumes: + - "./aspnetapp/WINGS/TlmCmd:/app/TlmCmd" + - "./aspnetapp/WINGS/Logs:/app/Logs" + environment: + ConnectionStrings__DefaultConnection: "server=mysql; uid=root; pwd=P@ssw0rd!; database=wings;" + ASPNETCORE_URLS: "https://+;http://+" + ASPNETCORE_HTTPS_PORT: 5001 + ASPNETCORE_Kestrel__Certificates__Default__Password: "P@ssw0rd!" + ASPNETCORE_Kestrel__Certificates__Default__Path: "/app/certificate.pfx" + ports: + - "5001:5001" + depends_on: + - mysql + mysql: + image: wings-mysql-img + build: ./mysql + container_name: wings-mysql-ctr + ports: + - "3306:3306" + volumes: + - "./mysql/init:/docker-entrypoint-initdb.d" + - "./mysql/data:/var/lib/mysql" + environment: + - MYSQL_DATABASE=wings + - MYSQL_ROOT_PASSWORD=P@ssw0rd! diff --git a/mysql/Dockerfile b/mysql/Dockerfile new file mode 100644 index 0000000..e9f53ff --- /dev/null +++ b/mysql/Dockerfile @@ -0,0 +1,23 @@ +FROM mysql:8.0 + +# Workaround: https://github.com/docker-library/mysql/issues/811 +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29 + +# Set debian default locale to ja_JP.UTF-8 +RUN apt-get update && \ + apt-get install -y locales && \ + rm -rf /var/lib/apt/lists/* && \ + echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \ + locale-gen ja_JP.UTF-8 +ENV LC_ALL ja_JP.UTF-8 + +# Set MySQL character +RUN { \ + echo '[mysqld]'; \ + echo 'character-set-server=utf8mb4'; \ + echo 'collation-server=utf8mb4_general_ci'; \ + echo '[client]'; \ + echo 'default-character-set=utf8mb4'; \ +} > /etc/mysql/conf.d/charset.cnf + +EXPOSE 3306 diff --git a/mysql/data/.gitignore b/mysql/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/mysql/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/mysql/init/init.sql b/mysql/init/init.sql new file mode 100644 index 0000000..1ae5ba0 --- /dev/null +++ b/mysql/init/init.sql @@ -0,0 +1,55 @@ +CREATE DATABASE IF NOT EXISTS `wings`; +USE `wings`; + +CREATE TABLE IF NOT EXISTS `__EFMigrationsHistory` ( + `MigrationId` varchar(95) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `ProductVersion` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + PRIMARY KEY (`MigrationId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `Components` ( + `Id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `Name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `TcPacketKey` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `TmPacketKey` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `LocalDirPath` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + PRIMARY KEY (`Id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `Operations` ( + `Id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `PathNumber` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `Comment` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `CreatedAt` datetime NOT NULL, + `IsRunning` tinyint(1) NOT NULL, + `IsTmtcConnected` tinyint(1) NOT NULL, + `FileLocation` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `TmtcTarget` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `ComponentId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + PRIMARY KEY (`Id`), + KEY `IX_Operations_ComponentId` (`ComponentId`), + CONSTRAINT `FK_Operations_Components_ComponentId` FOREIGN KEY (`ComponentId`) REFERENCES `Components` (`Id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `CommandLogs` ( + `SentAt` datetime(1) NOT NULL, + `OperationId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `ExecType` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `ExecTime` int unsigned NOT NULL, + `CmdName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `Param1` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `Param2` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `Param3` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `Param4` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `Param5` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `Param6` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + PRIMARY KEY (`OperationId`,`SentAt`), + CONSTRAINT `FK_CommandLogs_Operations_OperationId` FOREIGN KEY (`OperationId`) REFERENCES `Operations` (`Id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`) VALUES + ('20220330061702_InitialModels', '6.0.0'); + +INSERT INTO `Components` (`Id`, `Name`, `TcPacketKey`, `TmPacketKey`, `LocalDirPath`) VALUES + ('77bd0ce9-453a-4b8a-b474-d8be2faa3e8b', 'MOBC', 'OBC', 'OBC', 'TlmCmd/OBC'), + ('ff0cab42-7002-46f8-abbc-45d0ef82eb87', 'ISSL_COMMON', 'ISSL_COMMON', 'ISSL_COMMON', 'TlmCmd/ISSL_COMMON'); \ No newline at end of file From 9a8f6a818d4ec5153c3eaa7ba55e358e7361229d Mon Sep 17 00:00:00 2001 From: Kota Kakihara <56664912+kota-kakihara@users.noreply.github.com> Date: Tue, 5 Jul 2022 13:37:15 +0900 Subject: [PATCH 2/4] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3d0a42e..ba2db2a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # WINGS Web-based INterface Ground-station Software -WINGS is a software processing telemetry and command for satellites and satellite componetns. WINGS is a web application which can be used from both web browsers and http api requests. WINGS supports C2A-styled (https://github.com/ut-issl/c2a-core) and ISSL-styled telemetry and command formats. Users can implement other formats. -Usually, interface software is required for the connection between WINGS and satellites. WINGS_TMTC_IF is such software which supports COM port and socket connection. Users can implement other interface software. -WINGS uses ASP .NET (https://github.com/dotnet/aspnetcore) for backend software, MySQL for database, and React (https://github.com/facebook/react) for frontend software. +WINGS is a software processing telemetry and command for satellites and satellite componetns. WINGS is a web application which can be used from both web browsers and http api requests. WINGS supports [C2A](https://github.com/ut-issl/c2a-core)-styled and ISSL-styled telemetry and command formats. Users can implement other formats. +Usually, interface software is required for the connection between WINGS and satellites. [WINGS_TMTC_IF](https://github.com/ut-issl/wings-tmtc-if) is such software which supports COM port and socket connection. Users can implement other interface software. +WINGS uses [ASP .NET](https://github.com/dotnet/aspnetcore) for backend software, MySQL for database, and [React](https://github.com/facebook/react) for frontend software. ## Getting Started for User ### Prerequisites From 707a731c08d8ed0caa3bdf83eb501c9db0182e2f Mon Sep 17 00:00:00 2001 From: Ryo Suzumoto Date: Tue, 5 Jul 2022 14:45:22 +0900 Subject: [PATCH 3/4] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8bfb333 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Intelligent Space Systems Laboratory, The University of Tokyo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From e2a760aeb09e3082fdfc6dcbf3880c339feeddd2 Mon Sep 17 00:00:00 2001 From: Ryo Suzumoto Date: Tue, 5 Jul 2022 17:15:38 +0900 Subject: [PATCH 4/4] Update README.md --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ba2db2a..50eb5e9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # WINGS Web-based INterface Ground-station Software -WINGS is a software processing telemetry and command for satellites and satellite componetns. WINGS is a web application which can be used from both web browsers and http api requests. WINGS supports [C2A](https://github.com/ut-issl/c2a-core)-styled and ISSL-styled telemetry and command formats. Users can implement other formats. -Usually, interface software is required for the connection between WINGS and satellites. [WINGS_TMTC_IF](https://github.com/ut-issl/wings-tmtc-if) is such software which supports COM port and socket connection. Users can implement other interface software. +WINGS is a software processing telemetry and command for satellites and satellite components. WINGS is a web application that can be used from both web browsers and HTTP API requests. WINGS supports [C2A](https://github.com/ut-issl/c2a-core)-styled and ISSL-styled telemetry and command formats. Users can implement other formats. +Usually, interface software is required for the connection between WINGS and satellites. [WINGS_TMTC_IF](https://github.com/ut-issl/wings-tmtc-if) is such software that supports COM port and socket connection. Users can implement other interface software. WINGS uses [ASP .NET](https://github.com/dotnet/aspnetcore) for backend software, MySQL for database, and [React](https://github.com/facebook/react) for frontend software. -## Getting Started for User +## Getting Started for Users ### Prerequisites The application listed below is required: + [Docker](https://docs.docker.com/get-docker/) @@ -15,7 +15,7 @@ The application listed below is required: 2. Navigate to the desired location for the repository. 3. Clone the repository. 4. Make sure that docker is running. -5. Create dokcer images in the directory containing `docker-compose.yml`. +5. Create docker images in the directory containing `docker-compose.yml`. ``` docker-compose build ``` @@ -31,11 +31,11 @@ The application listed below is required: docker-compose down ``` ### Operation -1. Fulfill comment and select a component in main page. -2. Click operation start bottun. -3. Connect WINGS_TMTC_IF to the operation. -4. Click operation join bottun. -5. You can show telemetry and send command while joining operation. +1. Fulfill comment and select a component in the main page. +2. Click the operation start button. +3. Connect WINGS_TMTC_IF to the the operation. +4. Click the operation join button. +5. You can show telemetries and send commands while joining the operation. ### Command - You can send commands by clicking command line and pushing `"Shift" + "Return"` keys. @@ -43,7 +43,7 @@ The application listed below is required: - If you want to add unplanned commands, select command in "Command Selection" area and click "add" button. ### Telemetry -- You can show telemetry by cliking "+" bottun, selecting showing type and telemetry packet names, and cliking "Open" button. +- You can show telemetry by clicking "+" button, selecting showing type and telemetry packet names, and clicking "Open" button. - "packet" type just shows telemetry values. - "graph" type shows telemetry graphs. - You can save and restore telemetry showing layouts (click "Layouts" button).