├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile.dev ├── LICENSE.md ├── README.md ├── docker-compose.yml ├── docs ├── development.md ├── devices.md ├── docker.md ├── mediadevices │ ├── matroxcip.md │ └── riedelemb.md └── standalone.md ├── preview.jpg ├── server ├── config_example │ ├── mediadev_matroxcip │ │ ├── edid │ │ │ ├── 1080p50.bin │ │ │ └── 2160p50.bin │ │ └── matroxcip.json │ ├── settings.json │ ├── topology.json │ └── users.json ├── package-lock.json ├── package.json ├── src │ ├── lib │ │ ├── SyncServer │ │ │ ├── syncObject.ts │ │ │ ├── websocketClient.ts │ │ │ └── websocketSyncServer.ts │ │ ├── bitrateHelper │ │ │ ├── BitrateCalculator.ts │ │ │ ├── dmt_timings.ts │ │ │ └── vic_timings.ts │ │ ├── configState.ts │ │ ├── crosspointAbstraction.ts │ │ ├── crosspointUpdateThread.ts │ │ ├── functions.ts │ │ ├── mdnsService.ts │ │ ├── mediaDevices.ts │ │ ├── nmosConnector.ts │ │ ├── parseSettings.ts │ │ ├── syncLog.ts │ │ └── topology.ts │ ├── mediaDevices │ │ ├── imagineSnp.ts │ │ ├── matroxConvertIp.ts │ │ └── riedelEmbrionix.ts │ ├── networkDevices │ │ ├── aristaDCS7060XX2.ts │ │ ├── dummySwitch.ts │ │ ├── networkDevice.ts │ │ └── networkInfrastructureConnector.ts │ ├── server.ts │ └── service.ts └── tsconfig.json └── ui ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public ├── assets │ └── connectionWorker.js └── crosspoint.svg ├── safelist.txt ├── src ├── App.svelte ├── app.scss ├── assets │ └── svelte.svg ├── lib │ ├── Counter.svelte │ ├── InlineEditor.svelte │ ├── LetMeScroll.js │ ├── OverlayMenu │ │ ├── OverlayMenu.svelte │ │ └── OverlayMenuService.ts │ ├── PrettyJson.svelte │ ├── ScrollArea.svelte │ ├── ServerConnector │ │ ├── ServerConnectorOverlay.svelte │ │ └── ServerConnectorService.ts │ ├── SetupDevice.svelte │ ├── SetupFlow.svelte │ ├── SvelteFlowNodes │ │ ├── DeviceNode.svelte │ │ └── SwitchNode.svelte │ └── functions.ts ├── main.ts ├── routes │ ├── crosspoint.svelte │ ├── debug.svelte │ ├── details.svelte │ ├── devices.svelte │ ├── logging.svelte │ ├── mediadevices │ │ ├── matroxcip.svelte │ │ └── riedelembrionix.svelte │ ├── setup.svelte │ └── topology.svelte ├── scss │ ├── components.scss │ ├── components │ │ ├── datatable.scss │ │ ├── input.scss │ │ ├── prettyjson.scss │ │ └── scrollbars.scss │ ├── crosspoint.scss │ ├── debug.scss │ ├── details.scss │ ├── fonts │ │ ├── 400-italic.css │ │ ├── 400.css │ │ ├── 500-italic.css │ │ ├── 500.css │ │ ├── 700-italic.css │ │ ├── 700.css │ │ └── mono.css │ ├── layout.scss │ ├── setup.scss │ └── topology.scss └── vite-env.d.ts ├── svelte.config.js ├── tailwind.config.cjs ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | server/node_modules/ 2 | server/dist/* 3 | server/config/* 4 | server/config_backups/* 5 | server/state/* 6 | server/public/* 7 | server/log/* 8 | _tempFiles/* 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ### Version 2.1.0 ( no release yet ) 5 | - adding multicast management 6 | - fixing problem without auth (needs manual fix in config/users.json) 7 | - reconnect when flow settings changes (all changes in SDP file) 8 | - update logic for crosspoint numbers 9 | - ui for updateing crosspoint numbers 10 | - ui for multicast addresses 11 | 12 | ## Version 2.0.0 13 | - Complete redesign of the server core for supporting more features 14 | - Multithreaded Server 15 | - Complete redesign of the UI. (Switched from Angular to Svelte due to performance) 16 | - Added basics for implementation of device abstractions 17 | - bug fixing -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 AS base 2 | 3 | COPY ./ui /nmos-crosspoint/ui 4 | COPY ./server /nmos-crosspoint/server 5 | ENV PATH /nmos-crosspoint/server/node_modules/.bin:$PATH 6 | ENV PATH /nmos-crosspoint/ui/node_modules/.bin:$PATH 7 | 8 | 9 | 10 | WORKDIR /nmos-crosspoint/ui 11 | RUN npm install 12 | RUN npm run build 13 | 14 | WORKDIR /nmos-crosspoint/server 15 | RUN npm install -g typescript@latest 16 | RUN npm install 17 | RUN npm run build 18 | 19 | CMD cd /nmos-crosspoint/server && node ./dist/server.js 20 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:20 AS dev 2 | 3 | COPY ./ui /nmos-crosspoint/ui 4 | COPY ./server /nmos-crosspoint/server 5 | ENV PATH /nmos-crosspoint/server/node_modules/.bin:$PATH 6 | ENV PATH /app/node_modules/.bin:$PATH 7 | 8 | WORKDIR /nmos-crosspoint/ui 9 | RUN npm install 10 | RUN npm run build 11 | 12 | WORKDIR /nmos-crosspoint/server 13 | RUN npm install -g typescript@latest 14 | RUN npm install -g tsc-watch@latest 15 | RUN npm install 16 | 17 | CMD cd /nmos-crosspoint/server && npm run dev & cd /nmos-crosspoint/ui && npm run dev -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | Copyright © 2021 Johannes Grieb 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NMOS Crosspoint 2 | 3 | This tool is intended as a simple Orchestration layer for NMOS and ST2110 based Media Networks. 4 | 5 | ![preview.jpg](preview.jpg) 6 | 7 | This tool is tested against a lot of devices and now stable and performant with more than 2000 Flows. 8 | 9 | ## Features 10 | 11 | - List of all NMOS Devices and Flows in the Network 12 | - Connecting Flows to Receivers (Crosspoint style) 13 | - Integration for prorietary device overview and control 14 | - Connection to Companion (Beta) 15 | - Managing of all Multicast adresses (Beta) 16 | - Reconnect on flow changes 17 | 18 | ## Planned features 19 | 20 | - Virtual senders and receivers 21 | - Network topology view 22 | - Active, network aware routing (SDN Like) 23 | - IS-07 ( Connecting WebSocket Data Streams is already working with easy-nmos-node ) 24 | - IS-08 ( Work in progress) 25 | 26 | ## TODO 27 | 28 | - Documentation ! 29 | 30 | ## Changes from Version 1.0 31 | 32 | - Complete redesign of the server core for supporting more features 33 | - Multithreaded Server 34 | - Complete redesign of the UI. (Switched from Angular to Svelte due to performance) 35 | - Added basics for implementation of device abstractions 36 | - Lot of Bug fixing 37 | 38 | ## Dependencies 39 | 40 | This tool needs a working NMOS Registry running in the network. We test against [nmos-cpp](https://github.com/sony/nmos-cpp) in a docker container. 41 | 42 | To get one up and running, you can use the one provided by rhastie: [https://github.com/rhastie/build-nmos-cpp](https://github.com/rhastie/build-nmos-cpp) 43 | 44 | ## Configuration 45 | 46 | You can simply rename the `server/config_example` folder to `server/config` please see the config files for possible Settings, documentation has to be done. 47 | 48 | At startup on some installations there are some warnings about missing files, the System will create these files as soon as there is something to store. 49 | As there is no `state` folder on some installations these fiels are not created. Just create a state folder and the subfolders named in the warnings and you are fine. You do not need to create the files. 50 | 51 | There is a Bug with Authentification which does not allow unauthentificated access. Will be fixed soon, but for now you have to crete a password (SHA256 in the `users.json` file) 52 | 53 | ## Installation 54 | 55 | The simplest way to get NMOS Crosspoint up and running is to use Docker Compose. 56 | 57 | Make sure to change `docker-compose.yml` for your environment. 58 | ```shell 59 | docker-compose up 60 | ``` 61 | This will create and start one Docker Container with a node express server. 62 | Just point your Browser to the IP of the created Docker Container at port 80 63 | 64 | ## Just run it ! 65 | 66 | If you have a NMOS Registry in the network (Easy-NMOS for example) you can just start this tool on any computer. 67 | You will need an Installation of Node.js Version 20, change to the `server`folder and just run: `node ./dist/server.js`. 68 | 69 | ## Network 70 | 71 | NMOS Crosspoint can find and use multiple Registries, over all attached networks. Usually I test in an environment with the following networks: 72 | - OOB (Out of Band Management network) 73 | - Amber (Main Media network) 74 | - Blue (Backup Media network) 75 | 76 | NMOS Crosspoint can be connected to even more networks and will try to reach devices over multiple interfaces if they provide multiple endpoints to the registry. 77 | In theory, one should be able to get a complete failover. 78 | 79 | At this time, NMOS Crosspoint does not handle the multiple "Legs" (network interfaces) presented by NMOS in a inteligent way. So there is no mapping for subnets or any desicion which Legs can connect. 80 | Leg 1 of a sender is always connected to Leg 1 of a receiver, the SDP Manifest files are not modified in any way. 81 | There are plans to see the whole network topology and handle lots of these things. 82 | 83 | Unfortunately, some devices do not present their NMOS API on all interfaces. So for best compatibility, NMOS Crosspoint and the API should be present in all networks. If the NMOS Registry is configured manually in the devices, one can also use routing. 84 | 85 | ## How it works 86 | 87 | ```mermaid 88 | flowchart TD 89 | server[NMOS Crosspoint\nServer] <-- WebSocket\nUpdates, Commands --> client[Web UI] 90 | node1["NMOS Node\n(Device)"] 91 | node2["NMOS Node\n(Device)"] 92 | 93 | registry[NMOS\nRegistry] -- WebSocket\nDevices, Sender, Receivers, Updates\nmultiple Connections --> server 94 | node1 -- Updates --> registry 95 | node2 -- Updates --> registry 96 | 97 | server -- "Rest\nGET SDP File\nConnect (PATCH, activate_immediate)" --> node1 98 | server -- "Rest\nGET SDP File\nConnect (PATCH, activate_immediate)" --> node2 99 | 100 | 101 | ``` 102 | 103 | 104 | ## Development 105 | 106 | ``` 107 | docker-compose up nmos-crosspoint-dev 108 | ``` 109 | Will start one Docker Container with a live updating Node Server. 110 | For both folders, `/ui` and `/server` you could also run `npm install` and `npm run dev` for a local development session. 111 | 112 | In development mode it is extremely usefull for debugging as you can nearly live modify patch commands and the interpretation of NMOS data. under `http:///debug` you can see the full live updating crosspoint and NMOS data. Under `http:///log` there is lots of usefull data while making connections. 113 | 114 | ## Standalone 115 | 116 | It is possible to run this tool without docker. Still there is need for a NMOS Registry, nmos-cpp can be built and operated without Docker. 117 | 118 | ```shell 119 | # build the angular app 120 | cd ./ui 121 | npm install --force # force is required for ace (json rendering, to be fixed or replaced) 122 | npm run build 123 | cd .. 124 | 125 | # build the server (typescript has to be globally available) 126 | 127 | cd ./server 128 | npm install 129 | # optional: npm install -g typescript@latest 130 | tsc 131 | node ./dist/server.js 132 | ``` 133 | 134 | Check the `Dockerfile.dev` for information on how to start live development servers. 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | 5 | # Create NMOS Registry/Controller container instance 6 | nmos-crosspoint: 7 | build: 8 | context: './' 9 | container_name: nmos-crosspoint_v2 10 | hostname: nmos-crosspoint_v2 11 | domainname: local 12 | ports: 13 | - "80:80" 14 | volumes: 15 | - "./server/config:/nmos-crosspoint/server/config" 16 | - "./server/state:/nmos-crosspoint/server/state" 17 | networks: 18 | oob: 19 | ipv4_address: '10.1.0.20' 20 | amber: 21 | ipv4_address: '10.11.0.20' 22 | blue: 23 | ipv4_address: '10.12.0.20' 24 | 25 | nmos-crosspoint-dev: 26 | profiles: ["dev"] 27 | build: 28 | context: './' 29 | dockerfile: Dockerfile.dev 30 | container_name: nmos-crosspoint-dev 31 | hostname: nmos-crosspoint-dev 32 | domainname: local 33 | ports: 34 | - "80:80" 35 | - "5137:5137" 36 | volumes: 37 | - "./server:/nmos-crosspoint/server" 38 | - "./ui:/nmos-crosspoint/ui" 39 | - "/nmos-crosspoint/server/node_modules" 40 | - "/nmos-crosspoint/ui/node_modules" 41 | networks: 42 | oob: 43 | ipv4_address: '10.1.0.18' 44 | amber: 45 | ipv4_address: '10.11.0.18' 46 | blue: 47 | ipv4_address: '10.12.0.18' 48 | 49 | 50 | networks: 51 | oob: 52 | name: oob 53 | # Create external macvlan subnet using host physical interface allowing containers to have their own IP addresses 54 | driver: macvlan 55 | driver_opts: 56 | parent: ens192 57 | ipam: 58 | config: 59 | - subnet: 10.1.0.0/24 60 | amber: 61 | name: amber 62 | # Create external macvlan subnet using host physical interface allowing containers to have their own IP addresses 63 | driver: macvlan 64 | driver_opts: 65 | parent: ens224 66 | ipam: 67 | config: 68 | - subnet: 10.11.0.0/24 69 | blue: 70 | name: blue 71 | # Create external macvlan subnet using host physical interface allowing containers to have their own IP addresses 72 | driver: macvlan 73 | driver_opts: 74 | parent: ens256 75 | ipam: 76 | config: 77 | - subnet: 10.12.0.0/24 78 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3dmedium/nmos_crosspoint/18bcbf5aa97865070307d7c5d7c92ba1bfea5ce5/docs/development.md -------------------------------------------------------------------------------- /docs/devices.md: -------------------------------------------------------------------------------- 1 | # Device Specific Docs 2 | 3 | This is a list for known issues or device specific implementations, working arround issues or adding specific features. 4 | 5 | ## Matrox 6 | ### Matrox Convert IP (General) 7 | 8 | Device implementation available. Specific features like EDID and scaling is implemented. 9 | 10 | In SDP files sometimes there is "UNSPECIFIED" for color space or transfer curve. This leads to problems for some receivers. 11 | Therefore a fix for bad SDP files is added, one can enable this in `config.json` with `"fixSdpBugs":true`. 12 | UNSPECIFIED is then replacced by SDR or BT709. 13 | 14 | After a resolution in the SDI or HDMI Signal changes, the change is first given to the Regsitry. After some time the SDP file in the device will be updated. In some cases the update of the SDP file and reconnection is done before the Convert IP was up to date. Therefore the SDP files are pushed twice (delay of some seconds) after changes. 15 | 16 | 17 | ## Blackmagic 18 | 19 | ### Blackmagic 2110 IP Mini BiDirect 12G 20 | 21 | The bidirectional devices can not receive their own flows. A normal ethernet switch will not send back flows. Therefore a connected device needs a loopback mode for this. Seems to be not implemented. 22 | 23 | Extremely instable NMOS implementation. Some API calls lead to crashes. Reason for this needs to be analyzed. 24 | Making connections between Blackmagic devices is just fine. 25 | 26 | - After bad API requests most of the times SDP files can not be downloaded. So no further connections are possible. 27 | - Senders do not get a active state most of the times. 28 | - Activate a sender via API request crashes the NMOS api. 29 | - A lot of ECONNRESET responses for successful API requests (connection was successful). 30 | - Does not support UNSPECIFIED for color space or transfer system in SDP file. 31 | - Can not receive its own flows. (Loopback missing) 32 | - Registration to NMOS Registry is not stable. 33 | 34 | 35 | ## Imageine 36 | 37 | ### Selenio Network Processor 38 | The default NMOS Labels are rewritten for simplicity. 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3dmedium/nmos_crosspoint/18bcbf5aa97865070307d7c5d7c92ba1bfea5ce5/docs/docker.md -------------------------------------------------------------------------------- /docs/mediadevices/matroxcip.md: -------------------------------------------------------------------------------- 1 | # Mediadevice - Matrox CIP 2 | 3 | ## Config 4 | -------------------------------------------------------------------------------- /docs/mediadevices/riedelemb.md: -------------------------------------------------------------------------------- 1 | # Mediadevice - Riedel Embrionix based 2 | 3 | -------------------------------------------------------------------------------- /docs/standalone.md: -------------------------------------------------------------------------------- 1 | # Standalone operation 2 | 3 | ## Linux systemd service example 4 | 5 | ``` 6 | [Unit] 7 | Description=nmos_crosspoint 8 | Documentation=https://github.com/3dmedium/nmos_crosspoint 9 | After=network.target 10 | 11 | [Service] 12 | Type=simple 13 | User=nmos 14 | 15 | WorkingDirectory=/opt/nmos_crosspoint/server 16 | ExecStart=/usr/bin/node /opt/nmos_crosspoint/server/dist/server.js 17 | Restart=on-failure 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | ``` -------------------------------------------------------------------------------- /preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3dmedium/nmos_crosspoint/18bcbf5aa97865070307d7c5d7c92ba1bfea5ce5/preview.jpg -------------------------------------------------------------------------------- /server/config_example/mediadev_matroxcip/edid/1080p50.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3dmedium/nmos_crosspoint/18bcbf5aa97865070307d7c5d7c92ba1bfea5ce5/server/config_example/mediadev_matroxcip/edid/1080p50.bin -------------------------------------------------------------------------------- /server/config_example/mediadev_matroxcip/edid/2160p50.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3dmedium/nmos_crosspoint/18bcbf5aa97865070307d7c5d7c92ba1bfea5ce5/server/config_example/mediadev_matroxcip/edid/2160p50.bin -------------------------------------------------------------------------------- /server/config_example/mediadev_matroxcip/matroxcip.json: -------------------------------------------------------------------------------- 1 | { 2 | "user":"admin1", 3 | "password":"password", 4 | "closeExistingSessions":false, 5 | "ignoreHttps":true, 6 | "edids":[{ 7 | "name":"1080p50", 8 | "file":"./config/mediadev_matrox/edid/1080p50.bin" 9 | },{ 10 | "name":"2160p50", 11 | "file":"./config/mediadev_matrox/edid/2160p50.bin" 12 | }], 13 | "resolutions":[{ 14 | "name":"1080p50", 15 | "settings":{ 16 | "height":1080, 17 | "width":1920, 18 | "refreshRateDen":1, 19 | "refreshRateNum":50, 20 | "scan":"progressive" 21 | } 22 | },{ 23 | "name":"2160p50", 24 | "settings":{ 25 | "height":2160, 26 | "width":3840, 27 | "refreshRateDen":1, 28 | "refreshRateNum":50, 29 | "scan":"progressive" 30 | } 31 | }], 32 | "manualDevices":[ 33 | {"sn":"abc1234", "auth":{"user":"admin1","password":"password"}}, 34 | {"sn":"abc4567", "auth":{"user":"admin1","password":"password"}} 35 | ] 36 | } -------------------------------------------------------------------------------- /server/config_example/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "staticNmosRegistries":[ 3 | {"ip":"10.1.0.211","port":80,"priority":10, "domain":""} 4 | ], 5 | "logFiles":"./log/", 6 | "logOutput":true, 7 | "server":{ 8 | "port":80, 9 | "address":"0.0.0.0" 10 | }, 11 | "nmos":{ 12 | "registryVersions": ["v1.3","v1.2"], 13 | "connectVersions":["v1.1", "v1.0"] 14 | }, 15 | "mdns":{ 16 | "listen":"0.0.0.0" 17 | }, 18 | "multicastRanges":{ 19 | "video":{"primary":"239.120.0.0/16", "secondary":"239.120.0.0/16"}, 20 | "audio":{"primary":"239.130.0.0/16", "secondary":"239.130.0.0/16"}, 21 | "other":{"primary":"239.140.0.0/16", "secondary":"239.140.0.0/16"}, 22 | "jxsv":{"primary":"239.122.0.0/16", "secondary":"239.122.0.0/16"}, 23 | "videoUhd":{"primary":"239.121.0.0/16", "secondary":"239.121.0.0/16"} 24 | }, 25 | "ptp":{"domain":101,"preferedMasters":[]}, 26 | "disabledModules":{ 27 | "mediadevices":["imagineSnp"], 28 | "core":["topology"] 29 | }, 30 | "reconnectOnSdpChanges":false, 31 | "fixSdpBugs":false, 32 | "firstDynamicNumber":1000 33 | } -------------------------------------------------------------------------------- /server/config_example/topology.json: -------------------------------------------------------------------------------- 1 | { 2 | "networkDevices":[ 3 | { 4 | "name":"Arista DCS", 5 | "type":"aristaDCS7060XX2", 6 | "connect":"10.1.0.14", 7 | "auth":"admin::password" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /server/config_example/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "users":{ 3 | "admin":{"password":"passwordsha256","groups":["user","admin"]} 4 | }, 5 | "permissions":{ 6 | "global":{ 7 | "allowRead":{"users":["__noAuth"],"groups":["user"]}, 8 | "allowWrite":{"users":["__noAuth"],"groups":["user"]}, 9 | "denyRead":{"users":[],"groups":[]}, 10 | "denyWrite":{"users":[],"groups":[]} 11 | }, 12 | "public":{ 13 | "allowRead":{"users":["__noAuth"],"groups":["user"]}, 14 | "allowWrite":{"users":[],"groups":[]}, 15 | "denyRead":{"users":[],"groups":[]}, 16 | "denyWrite":{"users":[],"groups":[]} 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nmos_crosspoint_server", 3 | "version": "2.0", 4 | "author": "Johannes Grieb", 5 | "license": "MIT", 6 | "description": "", 7 | "main": "./dist/server.js", 8 | "scripts": { 9 | "build": "tsc", 10 | "run": "node ./dist/server.js", 11 | "devnmos": "tsc-watch --onSuccess \"node --inspect=0.0.0.0:9230 ./dist/server.js -debug -devnmos\" --onFailure \"echo WHOOPS! Server compilation failed\"", 12 | "dev": "tsc-watch --onSuccess \"node --inspect=0.0.0.0:9230 ./dist/server.js -debug\" --onFailure \"echo WHOOPS! Server compilation failed\"", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "dependencies": { 16 | "axios": "^1.6.7", 17 | "express": "^4.18.2", 18 | "fast-json-patch": "^3.1.1", 19 | "form-data": "^4.0.0", 20 | "multicast-dns": "^7.2.5", 21 | "promise.any": "^2.0.6", 22 | "rfc6902": "^5.1.1", 23 | "rxjs": "^7.8.1", 24 | "sdp-transform": "^2.14.2", 25 | "ws": "^8.16.0" 26 | }, 27 | "devDependencies": { 28 | "@types/multicast-dns": "^7.2.4", 29 | "@types/node": "^20.11.16", 30 | "@types/sdp-transform": "^2.4.9", 31 | "@types/ws": "^8.5.10", 32 | "tsc-watch": "^6.0.4", 33 | "typescript": "^5.3.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/src/lib/SyncServer/syncObject.ts: -------------------------------------------------------------------------------- 1 | /* 2 | NMOS Crosspoint 3 | Copyright (C) 2021 Johannes Grieb 4 | */ 5 | 6 | import { WebsocketClient } from "./websocketClient"; 7 | import { applyPatch, createPatch } from "rfc6902"; 8 | import * as jsonpatch from 'fast-json-patch'; 9 | 10 | interface ClientListEntry { 11 | client: WebsocketClient; 12 | objectId: string | number; 13 | } 14 | 15 | export class SyncObject { 16 | public requiredPermission = "global"; 17 | public name: string = ""; 18 | protected reading = {}; 19 | protected readWaiting = {}; 20 | public getName() { 21 | return this.name; 22 | } 23 | 24 | constructor(name: string, state:any = null) { 25 | if(state){ 26 | this.setState(state) 27 | } 28 | this.name = name; 29 | } 30 | 31 | protected state = {}; 32 | 33 | private clientList: ClientListEntry[] = []; 34 | subscribe(client: WebsocketClient, objectId: string | number = 0) { 35 | objectId = "" + objectId; 36 | if ( 37 | this.clientList.find((entry) => { 38 | return entry.client === client && entry.objectId == objectId; 39 | }) 40 | ) { 41 | // no double subscriptions ! 42 | } else { 43 | this.clientList.push({ client: client, objectId: objectId }); 44 | if (!this.state.hasOwnProperty(objectId)) { 45 | this.readState(objectId); 46 | } else { 47 | client.sendObjectSyncData(this.name, "init", objectId, this.state[objectId]); 48 | } 49 | } 50 | } 51 | unsubscribe(client: WebsocketClient, objectId: string | number = 0) { 52 | objectId = "" + objectId; 53 | this.clientList = this.clientList.filter((c) => { 54 | return !(c.client == client && c.objectId == objectId); 55 | }); 56 | } 57 | 58 | private send(action: string, data, objectId: string | number = 0) { 59 | objectId = "" + objectId; 60 | this.clientList.forEach((c) => { 61 | if (c.objectId == objectId) { 62 | c.client.sendObjectSyncData(this.name, action, objectId, data); 63 | } 64 | }); 65 | } 66 | 67 | getState(objectId: string | number = 0) { 68 | objectId = "" + objectId; 69 | if (!this.state.hasOwnProperty(objectId)) { 70 | this.readState(objectId); 71 | } 72 | // TODO 73 | return this.state[objectId]; 74 | } 75 | getStateCopy(objectId: string | number = 0) { 76 | objectId = "" + objectId; 77 | if (!this.state.hasOwnProperty(objectId)) { 78 | this.readState(objectId); 79 | } 80 | return jsonpatch.deepClone(this.state[objectId]); 81 | //return JSON.parse(JSON.stringify(this.state[objectId])); 82 | } 83 | 84 | private copyObject(obj) { 85 | return jsonpatch.deepClone(obj) 86 | //return JSON.parse(JSON.stringify(obj)); 87 | } 88 | 89 | setState(data, objectId: string | number = 0, clientReset = false) { 90 | objectId = "" + objectId; 91 | 92 | if(!this.state[objectId]){ 93 | this.state[objectId] = {}; 94 | } 95 | //let patch = createPatch(this.state[objectId], data); 96 | var patch = jsonpatch.compare(this.state[objectId], data); 97 | 98 | this.state[objectId] = this.copyObject(data); 99 | if (clientReset) { 100 | this.send("init", this.state[objectId], objectId); 101 | } else { 102 | if(Array.isArray(patch) && patch.length == 0){ 103 | }else{ 104 | this.send("patch", patch, objectId); 105 | } 106 | } 107 | } 108 | 109 | patchState(patch, objectId: string | number = 0) { 110 | objectId = "" + objectId; 111 | jsonpatch.applyPatch(this.state[objectId], patch); 112 | if(Array.isArray(patch) && patch.length == 0){ 113 | }else{ 114 | this.send("patch", patch, objectId); 115 | } 116 | } 117 | 118 | readState(objectId: string | number = 0) { 119 | objectId = "" + objectId; 120 | if (this.reading[objectId]) { 121 | return; 122 | } 123 | this.reading[objectId] = true; 124 | setImmediate(() => { 125 | this.reading[objectId] = false; 126 | this.setState({}, objectId, true); 127 | }); 128 | } 129 | 130 | startReadState(objectId) { 131 | if (this.reading[objectId]) { 132 | this.readWaiting[objectId] = true; 133 | return false; 134 | } 135 | this.reading[objectId] = true; 136 | return true; 137 | } 138 | endReadState(objectId, state = null, forceReset = false) { 139 | if (state != null) { 140 | if (this.state.hasOwnProperty(objectId) && !forceReset) { 141 | this.setState(state, objectId, false); 142 | } else { 143 | this.state[objectId] = {}; 144 | this.setState(state, objectId, true); 145 | } 146 | } 147 | this.reading[objectId] = false; 148 | if (this.readWaiting[objectId]) { 149 | this.readState(objectId); 150 | this.readWaiting[objectId] = false; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /server/src/lib/SyncServer/websocketClient.ts: -------------------------------------------------------------------------------- 1 | /* 2 | NMOS Crosspoint 3 | Copyright (C) 2021 Johannes Grieb 4 | */ 5 | 6 | import * as WebSocket from "ws"; 7 | 8 | import * as Crypto from "crypto"; 9 | 10 | import { WebsocketSyncServer } from "./websocketSyncServer"; 11 | 12 | export class WebsocketClient { 13 | private ws: WebSocket; 14 | 15 | private authSeed: string = ""; 16 | public user: string = "__noAuth" 17 | public sendPermissionDenied(type:string, data:any){ 18 | this.send({ 19 | type:"permissionDenied", 20 | requestType:type, 21 | data:data 22 | }) 23 | } 24 | 25 | 26 | private env = {}; 27 | public getEnv() { 28 | return this.env; 29 | } 30 | 31 | private subscriptionList = []; 32 | 33 | constructor(s: WebSocket, e: any) { 34 | this.ws = s; 35 | this.env = e; 36 | 37 | this.authSeed = Crypto.createHash('sha1').update(""+Math.random()).digest('hex'); 38 | 39 | this.ws.on("message", (text: string) => { 40 | if (text == "ping" && this.ws.OPEN) { 41 | this.ws.send("pong"); 42 | } else { 43 | try { 44 | const m = JSON.parse(text); 45 | this.processMessage(m); 46 | } catch (e) {} 47 | } 48 | }); 49 | this.ws.on("close", () => { 50 | WebsocketSyncServer.getInstance().disconnectClient(this); 51 | }); 52 | try{ 53 | this.ws.send(JSON.stringify({ 54 | type:"authseed", 55 | seed:this.authSeed 56 | })); 57 | }catch(e){} 58 | } 59 | 60 | destructor() {} 61 | isConnected() { 62 | return !this.ws.CLOSED; 63 | } 64 | 65 | private send(obj) { 66 | try{ 67 | if (this.ws.OPEN) { 68 | this.ws.send(JSON.stringify(obj)); 69 | } 70 | }catch(e){} 71 | } 72 | 73 | sendObjectSyncData(channelName: string, action: string, objectId: any, state: any) { 74 | this.send({ 75 | type: "sync", 76 | channel: channelName, 77 | objectId: objectId, 78 | action: action, 79 | data: state, 80 | }); 81 | } 82 | 83 | private processMessage(message) { 84 | switch (message.type) { 85 | case "request": 86 | this.processRequest(message); 87 | break; 88 | case "sync": 89 | case "unsync": 90 | this.processSync(message); 91 | break; 92 | case "auth": 93 | this.processAuth(message); 94 | break; 95 | } 96 | } 97 | 98 | processRequest(message) { 99 | WebsocketSyncServer.getInstance() 100 | .request(message, this) 101 | .then((data) => { 102 | this.send(data); 103 | }) 104 | .catch((e) => { 105 | this.send(e); 106 | }); 107 | } 108 | 109 | processSync(message) { 110 | if (message.type === "sync") { 111 | if (typeof message.channel === "string") { 112 | let objectId = 0; 113 | if (typeof message.objectId === "string" || typeof message.objectId === "number") { 114 | objectId = message.objectId; 115 | } 116 | WebsocketSyncServer.getInstance().subscribeSyncObject(message.channel, this, objectId); 117 | } 118 | } else if (message.type === "unsync") { 119 | if (typeof message.channel === "string") { 120 | let objectId = 0; 121 | if (typeof message.objectId === "string" || typeof message.objectId === "number") { 122 | objectId = message.objectId; 123 | } 124 | WebsocketSyncServer.getInstance().unsubscribeSyncObject(message.channel, this, objectId); 125 | } 126 | } 127 | } 128 | 129 | 130 | processAuth(message:any){ 131 | if(WebsocketSyncServer.getInstance().authData.users.hasOwnProperty(message.user)){ 132 | let proof = WebsocketSyncServer.getInstance().authData.users[message.user].password + this.authSeed 133 | proof = Crypto.createHash('sha256').update(proof).digest('hex'); 134 | 135 | if(proof == message.password){ 136 | this.user = message.user; 137 | this.send({type:"auth", user:message.user}); 138 | }else{ 139 | this.send({type:"authfailed"}); 140 | } 141 | }else{ 142 | this.send({type:"authfailed"}); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /server/src/lib/SyncServer/websocketSyncServer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | NMOS Crosspoint 3 | Copyright (C) 2021 Johannes Grieb 4 | */ 5 | 6 | import * as cluster from "cluster"; 7 | import * as WebSocket from "ws"; 8 | 9 | var path = require("path"); 10 | 11 | const express = require("express"); 12 | 13 | import { WebsocketClient } from "./websocketClient"; 14 | import { SyncObject } from "./syncObject"; 15 | import { SyncLog } from "../syncLog"; 16 | 17 | interface SyncObjectList { 18 | [key: string]: SyncObject; 19 | } 20 | 21 | export class WebsocketSyncServer { 22 | public static init(address:string, port:number) { 23 | WebsocketSyncServer.instance = new WebsocketSyncServer(address,port); 24 | } 25 | 26 | authData:any = { 27 | "users":[], 28 | "permissions":{ 29 | "global":{ 30 | "allowRead":{"users":[],"groups":[]}, 31 | "allowWrite":{"users":[],"groups":[]}, 32 | "denyRead":{"users":[],"groups":[]}, 33 | "denyWrite":{"users":[],"groups":[]} 34 | } 35 | } 36 | } 37 | public relaodAuthData(data:any){ 38 | this.authData = data; 39 | } 40 | 41 | private static instance: WebsocketSyncServer; 42 | 43 | private server: any; 44 | 45 | public static getInstance(): WebsocketSyncServer { 46 | if (!WebsocketSyncServer.instance) { 47 | return null; 48 | } 49 | return WebsocketSyncServer.instance; 50 | } 51 | private wss: WebSocket.Server = null; 52 | 53 | // TODO client List Cleanup on closed 54 | private clientList: WebsocketClient[] = []; 55 | 56 | private routeList = {}; 57 | private syncObjectList: SyncObjectList = {}; 58 | 59 | public addRoute(method, route, permission, func) { 60 | this.routeList[method + "_" + route] = {func,permission}; 61 | } 62 | public request(message, client: WebsocketClient) { 63 | return new Promise((resolve, reject) => { 64 | let route = message.route.split("/"); 65 | let routeBase = route[0]; 66 | let query = route.splice(1); 67 | if (this.routeList.hasOwnProperty(message.method + "_" + routeBase)) { 68 | // TODO Check Permission 69 | if(this.checkPermission(client.user, this.routeList[message.method + "_" + routeBase].permission, (message.method =="GET" ? false:true))){ 70 | this.routeList[message.method + "_" + routeBase].func(client, query, message.data) 71 | .then((data) => { 72 | resolve({ 73 | type: "response", 74 | method: message.method, 75 | id: message.id, 76 | status: data["status"] ? data["status"] : 200, 77 | message: data["message"] ? data["message"] : "ok", 78 | data: data["data"] ? data["data"] : {}, 79 | }); 80 | }) 81 | .catch((error) => { 82 | reject({ 83 | type: "response", 84 | method: message.method, 85 | id: message.id, 86 | status: error["status"] ? error["status"] : 404, 87 | message: error["message"] ? error["message"] : "not found", 88 | error: error["error"] ? error["error"] : {}, 89 | }); 90 | }); 91 | }else{ 92 | reject({ 93 | type: "response", 94 | method: message.method, 95 | id: message.id, 96 | status: 403, 97 | message: "permission denied", 98 | error: {}, 99 | }); 100 | } 101 | 102 | } else { 103 | // TODO Logging 104 | reject({ 105 | type: "response", 106 | method: message.method, 107 | id: message.id, 108 | status: 404, 109 | message: "not found", 110 | error: {}, 111 | }); 112 | } 113 | }); 114 | } 115 | 116 | public addSyncObject(route, permission, syncObject: SyncObject) { 117 | this.syncObjectList[route] = syncObject; 118 | syncObject.name = route; 119 | syncObject.requiredPermission = permission; 120 | } 121 | public updateSyncObject(name: string, action: string, data: any, objectId: string | number = 0) { 122 | if (this.syncObjectList.hasOwnProperty(name)) { 123 | switch (action) { 124 | case "reset": 125 | this.syncObjectList[name].setState(data, objectId); 126 | break; 127 | case "patch": 128 | this.syncObjectList[name].patchState(data, objectId); 129 | break; 130 | } 131 | } else { 132 | //TODO logging and response for not subscribed 133 | } 134 | } 135 | 136 | public subscribeSyncObject(name: string, client: WebsocketClient, objectId: string | number = 0) { 137 | if (this.syncObjectList.hasOwnProperty(name)) { 138 | if(this.checkPermission(client.user,this.syncObjectList[name].requiredPermission,false)){ 139 | this.syncObjectList[name].subscribe(client, objectId); 140 | }else{ 141 | client.sendPermissionDenied("sync",{name,objectId}); 142 | } 143 | } else { 144 | //TODO logging and response for not ??? 145 | } 146 | } 147 | 148 | public unsubscribeSyncObject(name: string, client: WebsocketClient, objectId: string | number = 0) { 149 | if (this.syncObjectList.hasOwnProperty(name)) { 150 | this.syncObjectList[name].unsubscribe(client, objectId); 151 | } else { 152 | //TODO logging and response for not subscribed 153 | } 154 | } 155 | 156 | disconnectClient(client: WebsocketClient) { 157 | const index = this.clientList.indexOf(client); 158 | if (index > -1) { 159 | this.clientList.splice(index, 1); 160 | } 161 | } 162 | 163 | private constructor(address:string, port:number) { 164 | this.wss = new WebSocket.Server({ noServer: true }); 165 | this.wss.on("connection", (ws) => { 166 | 167 | const client = new WebsocketClient(ws, {}); 168 | this.clientList.push(client); 169 | SyncLog.log("verbose", "Server", "New Client connected", {env:client.getEnv} ); 170 | }); 171 | 172 | this.server = express(); 173 | this.server.use("/*", (req, res, next) => { 174 | //res.setHeader("Cache-Control", "must-revalidate"); 175 | //res.setHeader("service-worker-allowed", "/"); 176 | next(); 177 | }); 178 | this.server.use("/assets/connectionWorker.js", (req, res, next) => { 179 | res.setHeader("service-worker-allowed", "/"); 180 | next(); 181 | }); 182 | this.server.use(express.static("./public")); 183 | this.server.all("/*", function (req, res, next) { 184 | res.sendFile(path.resolve("./") + "/public/index.html"); 185 | }); 186 | 187 | const ls = this.server.listen(port, address, (e) => { 188 | SyncLog.log("error", "Server", "Failed to start on: "+address+":"+port, e ) 189 | },() => { 190 | SyncLog.log("info", "Server", "Listening to: "+address+":"+port ) 191 | }); 192 | 193 | ls.on("upgrade", (request, socket, head) => { 194 | this.wss.handleUpgrade(request, socket, head, (socket) => { 195 | this.wss.emit("connection", socket, request); 196 | }); 197 | }); 198 | } 199 | 200 | 201 | public checkPermission(user:string,required:string, write:boolean){ 202 | 203 | try{ 204 | let permis = this.authData.permissions[required]; 205 | 206 | let groups = []; 207 | if(user!="__noAuth"){ 208 | groups = this.authData.users[user].groups; 209 | } 210 | 211 | if(write){ 212 | if( permis.denyWrite.users.includes(user) || permis.denyWrite.groups.some((v)=>groups.includes(v))){ 213 | return false; 214 | } 215 | if( permis.allowWrite.users.includes(user) || permis.allowWrite.groups.some((v)=>groups.includes(v))){ 216 | return true; 217 | } 218 | }else{ 219 | if( permis.denyRead.users.includes(user) || permis.denyRead.groups.some((v)=>groups.includes(v))){ 220 | return false; 221 | } 222 | if( permis.allowRead.users.includes(user) || permis.allowRead.groups.some((v)=>groups.includes(v))){ 223 | return true; 224 | } 225 | } 226 | 227 | 228 | }catch(e){ 229 | console.log(e) 230 | } 231 | return false; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /server/src/lib/bitrateHelper/BitrateCalculator.ts: -------------------------------------------------------------------------------- 1 | export class BitrateCalculator { 2 | static dmt = require('./dmt_timings.js'); 3 | static vic = require('./vic_timings.js'); 4 | 5 | static audioFlow = { 6 | encoding:"raw", 7 | sampleRate:48000 , 8 | channels:2, 9 | depth:24, 10 | samplesPerPacket:48, 11 | vlan:false, 12 | }; 13 | 14 | static videoFlow = { 15 | encoding:"raw", 16 | width:3840 , 17 | height:2160, 18 | fps:50, 19 | interlaced:false, 20 | depth:10, 21 | sampling:"YCbCr422", 22 | gapped: true, 23 | gpm:false, 24 | shape:"narrow-linear", 25 | vlan:false, 26 | 27 | blanking:"dmt" 28 | }; 29 | 30 | static timing_proto = { 31 | valid : false, 32 | source : "none", 33 | h_active: 0, 34 | v_active: 0, 35 | interlaced: false, 36 | h_total: 0, 37 | h_blank: 0, 38 | v_total: 0, 39 | v_blank: 0 40 | }; 41 | 42 | 43 | // TODO Licesnsing DMT https://tomverbeure.github.io/video_timings_calculator 44 | static lookupDmt (horiz_pixels, vert_pixels, refresh_rate, interlaced){ 45 | let t = JSON.parse(JSON.stringify(BitrateCalculator.timing_proto)); 46 | t.interlaced = interlaced; 47 | 48 | 49 | 50 | BitrateCalculator.dmt.timings.forEach(function(timing, index){ 51 | if ( timing['h_active'] == horiz_pixels 52 | && timing['v_active'] == vert_pixels 53 | && timing['v_freq'].toFixed() == refresh_rate.toFixed() 54 | && timing['interlaced'] == interlaced 55 | ){ 56 | t.h_active = horiz_pixels; 57 | t.h_blank = timing['h_blank']; 58 | t.h_total = timing['h_total']; 59 | 60 | t.v_active = vert_pixels; 61 | t.v_blank = timing['v_blank']; 62 | t.v_total = timing['v_total']; 63 | 64 | t.valid = true; 65 | t.source = "dmt" 66 | } 67 | }); 68 | 69 | return t; 70 | } 71 | static lookupVic(horiz_pixels, vert_pixels, refresh_rate, interlaced){ 72 | let t = JSON.parse(JSON.stringify(BitrateCalculator.timing_proto)); 73 | t.interlaced = interlaced; 74 | BitrateCalculator.vic.timings.forEach(function(timing, index){ 75 | 76 | if ( timing['h_active'] == horiz_pixels 77 | && timing['v_active'] == vert_pixels 78 | && timing['v_freq'].toFixed() == refresh_rate.toFixed() 79 | && timing['interlaced'] == interlaced 80 | ){ 81 | t.h_active = horiz_pixels; 82 | t.h_blank = timing['h_blank']; 83 | t.h_total = timing['h_total']; 84 | 85 | t.v_active = vert_pixels; 86 | t.v_blank = timing['v_blank']; 87 | t.v_total = timing['v_total']; 88 | 89 | t.valid = true; 90 | t.source = "vic" 91 | } 92 | }); 93 | 94 | return t; 95 | } 96 | 97 | 98 | static calculateTiming (horiz_pixels, vert_pixels, refresh_rate, interlaced, mode ="cvt"){ 99 | let t = JSON.parse(JSON.stringify(BitrateCalculator.timing_proto)); 100 | t.interlaced = interlaced; 101 | t.h_active = horiz_pixels; 102 | t.v_active = vert_pixels; 103 | 104 | 105 | 106 | t.h_blank = 160; 107 | 108 | let field_rate = interlaced ? refresh_rate * 2 : refresh_rate; 109 | let lines = interlaced ? vert_pixels / 2 : vert_pixels; 110 | 111 | let min_v_porch = 3 112 | let min_v_bporch = 6; 113 | let v_sync = 10; // Largest Value for all aspects 114 | let min_vsync_bp = 550; 115 | 116 | 117 | if(mode == "rb"){ 118 | 119 | }else if(mode == "rbv2"){ 120 | t.h_blank = 80; 121 | v_sync = 8; 122 | }else{ 123 | //mode cvt 124 | 125 | 126 | let h_period_estimate = ((1 / field_rate) - min_vsync_bp / 1000000.0) / (lines + min_v_porch + interlaced) * 1000000.0; 127 | 128 | let IDEAL_DUTY_CYCLE = 30 - (300 * h_period_estimate/1000); 129 | 130 | if (IDEAL_DUTY_CYCLE < 20){ 131 | t.h_blank = Math.floor(vert_pixels * 20 / (100-20) / (2 * 8)) * (2 * 8); 132 | }else{ 133 | t.h_blank = Math.floor(vert_pixels * IDEAL_DUTY_CYCLE / (100 - IDEAL_DUTY_CYCLE) / (2 * 8)) * (2 * 8); 134 | } 135 | 136 | t.h_blank = Math.floor(vert_pixels * 20 / (100-20) / (2 * 8)) * (2 * 8); 137 | 138 | let v_sync_bp = Math.floor(min_vsync_bp / h_period_estimate) + 1; 139 | if (v_sync_bp < (v_sync + min_v_bporch)){ 140 | v_sync_bp = v_sync + min_v_bporch; 141 | } 142 | 143 | t.v_blank = v_sync_bp + min_v_porch; 144 | } 145 | 146 | t.h_total = t.h_active + t.h_blank; 147 | t.v_total = t.v_active + t.v_blank; 148 | t.valid = true; 149 | t.source = "calc" 150 | return t; 151 | } 152 | 153 | 154 | 155 | 156 | 157 | static calculateVideo(f){ 158 | // Constants 159 | let headerSize = { 160 | physical:8 + 4 + 12, 161 | ethernet:14, 162 | ip:20, 163 | udp:8, 164 | rtp:16+6, 165 | payload:1400 166 | }; 167 | if(f.vlan){ 168 | headerSize.ethernet = headerSize.ethernet + 4; 169 | } 170 | let o = { 171 | averageEthernet:0, 172 | averagePacket:0, 173 | averageMedia:0, 174 | pps:0, ppf:0, packetSize:0, 175 | 176 | maxRate:0, 177 | bufferRate:0, 178 | bufferSize:0, 179 | 180 | interPacketTimeAvg:0, 181 | interPacketTimeMin:0, 182 | interPacketTimeLine:0, 183 | interPacketTimeFrame:0, 184 | }; 185 | let height = f.height; 186 | 187 | 188 | let pixelEncodingBlockBytes = 0; 189 | let pixelEncodingBlockPixels = 0; 190 | 191 | let samplingdepth = ""+f.sampling+f.depth; 192 | 193 | switch(samplingdepth){ 194 | case "YCbCr42210": 195 | pixelEncodingBlockBytes = 5; 196 | pixelEncodingBlockPixels = 2; 197 | break; 198 | case "YCbCr4228": 199 | pixelEncodingBlockBytes = 4; 200 | pixelEncodingBlockPixels = 2; 201 | break; 202 | case "RGB4448": 203 | pixelEncodingBlockBytes = 3; 204 | pixelEncodingBlockPixels = 1; 205 | break; 206 | case "RGB44410": 207 | pixelEncodingBlockBytes = 15; 208 | pixelEncodingBlockPixels = 4; 209 | break; 210 | case "RGB44412": 211 | pixelEncodingBlockBytes = 9; 212 | pixelEncodingBlockPixels = 2; 213 | break; 214 | } 215 | 216 | 217 | if(pixelEncodingBlockPixels == 0){ 218 | console.error("Pixel Sampling ot Depth not supported." + samplingdepth); 219 | return o; 220 | } 221 | 222 | if(f.width % pixelEncodingBlockPixels != 0){ 223 | console.error("Width can not be encoded in given Sampling.") 224 | return o; 225 | } 226 | 227 | let maxPixelsPerPacket = Math.floor(headerSize.payload/pixelEncodingBlockBytes)*pixelEncodingBlockPixels; 228 | if(!f.gpm){ 229 | //Block Packing Mode 230 | let num = 0; 231 | for( num = Math.floor(f.width / maxPixelsPerPacket)+1; f.width % num != 0; num++){ 232 | 233 | } 234 | 235 | maxPixelsPerPacket = f.width / num; 236 | 237 | } 238 | 239 | let packetsPerFrame = Math.floor(f.width*height/maxPixelsPerPacket); 240 | if(f.gpm){ 241 | packetsPerFrame++; 242 | } 243 | let totalPayloadSize = f.width*height*pixelEncodingBlockBytes/pixelEncodingBlockPixels; 244 | 245 | let totalPacketSize = totalPayloadSize + (packetsPerFrame * (headerSize.ethernet + headerSize.ip + headerSize.udp + headerSize.rtp)); 246 | let totalFrameSize = totalPayloadSize + (packetsPerFrame * (headerSize.physical + headerSize.ethernet + headerSize.ip + headerSize.udp + headerSize.rtp)); 247 | 248 | o.pps = packetsPerFrame * f.fps; 249 | o.ppf = packetsPerFrame; 250 | o.averagePacket = totalPacketSize * f.fps * 8; 251 | o.averageEthernet = totalFrameSize * f.fps * 8; 252 | o.averageMedia = totalPayloadSize * f.fps * 8; 253 | o.packetSize = maxPixelsPerPacket * pixelEncodingBlockBytes/pixelEncodingBlockPixels + (headerSize.ethernet + headerSize.ip + headerSize.udp + headerSize.rtp) 254 | 255 | 256 | // Timings 257 | let timing = BitrateCalculator.lookupVic(f.width, f.height, f.fps, f.interlaced); 258 | if(!timing.valid){ 259 | timing = BitrateCalculator.lookupDmt(f.width, height, f.fps, f.interlaced); 260 | } 261 | if(!timing.valid){ 262 | timing = BitrateCalculator.calculateTiming(f.width, height, f.fps, f.interlaced); 263 | } 264 | 265 | let pixelsPerSecond = timing.h_total * timing.v_total * f.fps; 266 | let activePixelsPerSecond = timing.h_active * timing.v_active * f.fps; 267 | o.interPacketTimeAvg = 1 / (packetsPerFrame * f.fps); 268 | o.interPacketTimeMin = 1 /( pixelsPerSecond/maxPixelsPerPacket); 269 | o.interPacketTimeLine = 1/pixelsPerSecond * (timing.h_total - timing.h_active + maxPixelsPerPacket); 270 | o.interPacketTimeFrame = 1/pixelsPerSecond * (timing.h_total - timing.h_active + maxPixelsPerPacket + ( (timing.v_total - timing.v_active) * timing.h_total ) ); 271 | 272 | // TODO Differences for narrow linear 273 | 274 | // Calculate Max Rate 275 | o.maxRate = o.averageEthernet / activePixelsPerSecond * pixelsPerSecond; 276 | 277 | // Calculate Buffer Rate 278 | o.bufferRate = o.averageEthernet / timing.v_active * timing.v_total; 279 | if(f.shape == "narrow-linear"){ 280 | o.maxRate = o.bufferRate; 281 | o.interPacketTimeMin = 1 / (packetsPerFrame * f.fps / timing.v_active * timing.v_total); 282 | o.interPacketTimeLine = o.interPacketTimeMin; 283 | if(!f.gapped){ 284 | o.maxRate = o.averageEthernet; 285 | o.bufferRate = o.averageEthernet 286 | } 287 | } 288 | 289 | 290 | // Calculate Buffer Size (2 Lines) 291 | o.bufferSize = o.bufferRate / f.fps / timing.v_total * 2 / 8; 292 | if(f.shape == "wide"){ 293 | // TODO Half a frame ???? 294 | o.bufferSize = o.bufferRate / f.fps / 2 / 8; 295 | } 296 | 297 | return o; 298 | }; 299 | 300 | static calculateAudio(f){ 301 | // Constants 302 | let headerSize = { 303 | physical:8 + 4 + 12, 304 | ethernet:14, 305 | ip:20, 306 | udp:8, 307 | rtp:16, 308 | payload:1400 309 | }; 310 | if(f.vlan){ 311 | headerSize.ethernet = headerSize.ethernet + 4; 312 | } 313 | let o = { 314 | averageEthernet:0, 315 | averagePacket:0, 316 | averageMedia:0, 317 | pps:0, ppf:0, packetSize:0, 318 | 319 | maxRate:0, 320 | bufferRate:0, 321 | bufferSize:0, 322 | 323 | interPacketTimeAvg:0, 324 | interPacketTimeMin:0, 325 | interPacketTimeLine:0, 326 | interPacketTimeFrame:0, 327 | }; 328 | let bytesPerSample = Math.floor((f.depth+1) / 8); 329 | let totalPayloadSize = f.sampleRate * f.channels * bytesPerSample; 330 | 331 | o.pps = f.sampleRate / f.samplesPerPacket; 332 | 333 | let totalPacketSize = totalPayloadSize + (o.pps * (headerSize.ethernet + headerSize.ip + headerSize.udp + headerSize.rtp)); 334 | let totalFrameSize = totalPayloadSize + (o.pps * (headerSize.physical + headerSize.ethernet + headerSize.ip + headerSize.udp + headerSize.rtp)); 335 | 336 | 337 | 338 | 339 | o.ppf = 1; 340 | 341 | o.averagePacket = totalPacketSize * 8; 342 | o.averageEthernet = totalFrameSize * 8; 343 | o.averageMedia = totalPayloadSize * 8; 344 | o.packetSize = (f.samplesPerPacket * f.channels * bytesPerSample) + (headerSize.ethernet + headerSize.ip + headerSize.udp + headerSize.rtp); 345 | 346 | 347 | 348 | o.interPacketTimeAvg = 1 / o.pps; 349 | o.interPacketTimeMin = o.interPacketTimeAvg 350 | o.interPacketTimeLine = o.interPacketTimeAvg 351 | o.interPacketTimeFrame = o.interPacketTimeAvg 352 | 353 | // TODO Differences for narrow linear 354 | 355 | // Calculate Max Rate 356 | o.maxRate = o.averageEthernet; 357 | 358 | // Calculate Buffer Rate 359 | o.bufferRate = o.averageEthernet; 360 | 361 | 362 | // Calculate Buffer Size //TODO ho large ??? 363 | // 0.25 seconds ???? 364 | o.bufferSize = totalFrameSize * o.pps * 0.25; 365 | 366 | return o; 367 | }; 368 | } -------------------------------------------------------------------------------- /server/src/lib/configState.ts: -------------------------------------------------------------------------------- 1 | import { SyncObject } from "./SyncServer/syncObject"; 2 | import { SyncLog } from "./syncLog"; 3 | 4 | export class ConfigState { 5 | public syncCrosspoint: SyncObject; 6 | crosspointState: CrosspointState = {sources:[], destinations:[]}; 7 | 8 | constructor(){ 9 | if(ConfigState.instance == null){ 10 | ConfigState.instance = this; 11 | } 12 | 13 | } 14 | 15 | alias = {}; 16 | 17 | public static instance: ConfigState | null; 18 | } 19 | 20 | 21 | 22 | 23 | interface CrosspointFlow { 24 | id:string, 25 | virtual:boolean, 26 | number:number, 27 | name:string, 28 | niceName:string, 29 | sourceId:string, 30 | type:"video" | "audio" | "anc" | "mqtt" | "websocket" | "audiochannel", 31 | format: string, 32 | capabilities:string, 33 | channelNumber: number, 34 | sourceNumber: number, 35 | }; 36 | 37 | interface CrosspointDevice { 38 | id:string, 39 | virtual:boolean, 40 | number:number, 41 | name:string, 42 | niceName:string, 43 | flows: CrosspointFlow[] 44 | } 45 | export interface CrosspointState { 46 | sources: CrosspointDevice[], 47 | destinations: CrosspointDevice[] 48 | } 49 | -------------------------------------------------------------------------------- /server/src/lib/functions.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export function ComplexCompare(a:string,b:string){ 5 | 6 | let done = false; 7 | 8 | let apos = 0; 9 | let bpos = 0; 10 | let amax = a.length 11 | let bmax = b.length 12 | 13 | let anum = ""; 14 | let bnum = ""; 15 | 16 | let numbers = ["0","1","2","3","4","5","6","7","8","9"] 17 | 18 | let comp = 0; 19 | 20 | while(!done){ 21 | if(apos { 19 | this.hooks.forEach((hook)=>{ 20 | hook(response); 21 | }) 22 | }) 23 | } 24 | hooks:any[] = []; 25 | 26 | 27 | static registerHook(callback){ 28 | MdnsService.instance.hooks.push(callback); 29 | } 30 | 31 | static query(request){ 32 | MdnsService.instance.mdns.query(request); 33 | } 34 | 35 | static instance:MdnsService 36 | } -------------------------------------------------------------------------------- /server/src/lib/mediaDevices.ts: -------------------------------------------------------------------------------- 1 | import { SyncObject } from "./SyncServer/syncObject"; 2 | import { WebsocketSyncServer } from "./SyncServer/websocketSyncServer"; 3 | import { SyncLog } from "./syncLog"; 4 | 5 | const fs = require("fs"); 6 | 7 | export class MediaDevices { 8 | public syncDeviceList: SyncObject; 9 | deviceList:any = {deviceTypes:[]}; 10 | deviceHandlers:any[] = []; 11 | 12 | constructor(settings:any){ 13 | this.syncDeviceList = new SyncObject("mediadevices", this.deviceList); 14 | 15 | let server = WebsocketSyncServer.getInstance(); 16 | server.addSyncObject("mediadevices","global",this.syncDeviceList); 17 | 18 | let modDisabled:any=[]; 19 | 20 | try{ 21 | if(settings.hasOwnProperty("disabledModules") && settings.disabledModules.hasOwnProperty("mediadevices")){ 22 | settings.disabledModules.mediadevices.forEach((m)=>{ 23 | let name = ""+m; 24 | modDisabled.push(name); 25 | }); 26 | } 27 | }catch(e){} 28 | 29 | let path = "./dist/mediaDevices/" 30 | try{ 31 | let folder = fs.readdirSync(path, {withFileTypes: true}) 32 | folder.forEach((f)=>{ 33 | if(!f.isDirectory()){ 34 | 35 | let modName = f.name.split(".")[0]; 36 | let fext = f.name.split(".")[1]; 37 | //let modName = f.name.slice(0,f.name.length-3); 38 | let fext_last = f.name.slice(f.name.length-2) 39 | if(fext == "js" && fext_last == "js"){ 40 | // Load Extension 41 | if(modDisabled.includes(modName)){ 42 | SyncLog.log("info", "MediaDevices", "Disabled MediaDevice from: "+f.name) 43 | }else{ 44 | try{ 45 | let mediaDevClass = require("../../"+f.path + f.name).default; 46 | this.deviceHandlers.push(new mediaDevClass(settings)); 47 | SyncLog.log("info", "MediaDevices", "Load MediaDevice from: "+f.name) 48 | }catch(e){ 49 | SyncLog.log("error", "MediaDevices", "Can not load from: "+f.name, e) 50 | } 51 | } 52 | } 53 | } 54 | }) 55 | }catch(e){ 56 | SyncLog.log("error", "MediaDevices", "can not read folder: "+path, e) 57 | } 58 | 59 | 60 | this.deviceHandlers.forEach((h,i)=>{ 61 | 62 | h.quickState.subscribe((v)=>{ 63 | this.deviceList.deviceTypes[i] = v; 64 | this.syncDeviceList.setState(this.deviceList) 65 | }) 66 | }) 67 | 68 | 69 | this.syncDeviceList.setState(this.deviceList); 70 | } 71 | 72 | } 73 | 74 | 75 | 76 | export class MediaDeviceInterface { 77 | 78 | 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /server/src/lib/parseSettings.ts: -------------------------------------------------------------------------------- 1 | export function parseSettings(settings:any){ 2 | 3 | 4 | if(!settings.hasOwnProperty("reconnectOnSdpChanges")){ 5 | settings.reconnectOnSdpChanges = false; 6 | }else{ 7 | if(typeof settings.reconnectOnSdpChanges != "boolean"){ 8 | settings.reconnectOnSdpChanges = false; 9 | } 10 | } 11 | 12 | 13 | if(!settings.hasOwnProperty("fixSdpBugs")){ 14 | settings.fixSdpBugs = false; 15 | }else{ 16 | if(typeof settings.fixSdpBugs != "boolean"){ 17 | settings.fixSdpBugs = false; 18 | } 19 | } 20 | 21 | 22 | if(!settings.hasOwnProperty("autoMulticast")){ 23 | settings.autoMulticast = false; 24 | }else{ 25 | if(typeof settings.autoMulticast != "boolean"){ 26 | settings.autoMulticast = false; 27 | } 28 | } 29 | 30 | 31 | if(!settings.hasOwnProperty("firstDynamicNumber")){ 32 | settings.firstDynamicNumber = 1000; 33 | }else{ 34 | if(typeof settings.firstDynamicNumber != "number"){ 35 | settings.firstDynamicNumber = 1000; 36 | }else{ 37 | settings.firstDynamicNumber = Number.parseInt(settings.firstDynamicNumber); 38 | } 39 | 40 | if(settings.firstDynamicNumber < 1){ 41 | settings.firstDynamicNumber = 1000; 42 | } 43 | } 44 | 45 | 46 | 47 | 48 | return settings; 49 | } -------------------------------------------------------------------------------- /server/src/lib/syncLog.ts: -------------------------------------------------------------------------------- 1 | /* 2 | NMOS Crosspoint 3 | Copyright (C) 2021 Johannes Grieb 4 | */ 5 | 6 | import * as WebSocket from "ws"; 7 | import axios from "axios"; 8 | import { SyncObject } from "./SyncServer/syncObject"; 9 | import { Subject } from "rxjs"; 10 | import { WebsocketClient } from "./SyncServer/websocketClient"; 11 | 12 | export class SyncLog extends SyncObject { 13 | static instance: SyncLog; 14 | 15 | 16 | static error(topic: string,text: string, raw: any= null) { 17 | return SyncLog.log("error", topic,text, raw); 18 | } 19 | static warning(topic: string,text: string, raw: any= null) { 20 | return SyncLog.log("warning", topic,text, raw); 21 | } 22 | static info(topic: string,text: string, raw: any= null) { 23 | return SyncLog.log("info", topic,text, raw); 24 | } 25 | static debug(topic: string,text: string, raw: any= null) { 26 | return SyncLog.log("debug", topic,text, raw); 27 | } 28 | static verbose(topic: string,text: string, raw: any= null) { 29 | return SyncLog.log("verbose", topic,text, raw); 30 | } 31 | 32 | 33 | static log(severity: string, topic: string,text: string, raw: any= null) { 34 | let time = new Date().getTime(); 35 | let date = new Date(time).toISOString(); 36 | 37 | 38 | if(SyncLog.consoleDebug || severity == "error"){ 39 | console.log(date + " - " +severity + " " + topic +" - " + text); 40 | if(raw){ 41 | console.log(JSON.stringify(raw,null,2)); 42 | } 43 | 44 | } 45 | 46 | 47 | if (SyncLog.instance) { 48 | let id = SyncLog.instance.lastLogId++; 49 | SyncLog.instance.pushMessage(id, time, severity,topic, text, raw); 50 | return id; 51 | } else { 52 | return -1; 53 | } 54 | } 55 | 56 | constructor() { 57 | super("log"); 58 | this.setState({logList:[],lastLogId:0}) 59 | SyncLog.consoleDebug = true; 60 | SyncLog.instance = this; 61 | 62 | } 63 | 64 | setOutput(active:boolean){ 65 | SyncLog.consoleDebug = active; 66 | } 67 | 68 | private static logFile = ""; 69 | private static consoleDebug = false; 70 | 71 | 72 | limitHistory = 200; 73 | limitHistoryMem = 20000; 74 | logHistory = []; 75 | lastLogId = 0; 76 | 77 | 78 | readState(objectId) { 79 | objectId = "" + objectId; 80 | if (!this.startReadState(objectId)) { 81 | return; 82 | } 83 | this.endReadState(objectId, { logList: [] }); 84 | } 85 | pushMessage(id:number, time:number, severity: string, topic: string, text: string, raw: any) { 86 | let message = { 87 | id:id, 88 | time: time, 89 | severity, 90 | text, 91 | topic, 92 | raw, 93 | }; 94 | 95 | let state = this.getStateCopy(); 96 | 97 | this.logHistory.push(message); 98 | if (this.logHistory.length > this.limitHistoryMem) { 99 | this.logHistory.shift(); 100 | } 101 | state.logList.push(message); 102 | if (state.logList.length > this.limitHistory) { 103 | state.logList.shift(); 104 | } 105 | 106 | state.lastLogId = message.id; 107 | 108 | this.setState(state); 109 | } 110 | } 111 | 112 | export class LoggedError extends Error { 113 | constructor(msg: string, logId:number|string = "") { 114 | super(msg); 115 | 116 | this.logId = ""+logId 117 | // Set the prototype explicitly. 118 | Object.setPrototypeOf(this, LoggedError.prototype); 119 | } 120 | logId:string = ""; 121 | } 122 | -------------------------------------------------------------------------------- /server/src/lib/topology.ts: -------------------------------------------------------------------------------- 1 | import { SyncObject } from "./SyncServer/syncObject"; 2 | import { SyncLog } from "./syncLog"; 3 | 4 | import { NetworkInfrastructureAbstrraction, NetworkAuth,NetworkDevice,NetworkInfrastructure } from "../networkDevices/networkDevice"; 5 | import { WebsocketSyncServer } from "./SyncServer/websocketSyncServer"; 6 | 7 | 8 | 9 | const fs = require("fs"); 10 | 11 | export class Topology { 12 | public static instance: Topology | null; 13 | 14 | public syncTopology: SyncObject; 15 | 16 | nmosState:any; 17 | networkDevices: NetworkAuth[] = []; 18 | abstractions:NetworkInfrastructureAbstrraction[] = []; 19 | topologyState: TopologyState = {devices:[], infrastructure:[]}; 20 | 21 | constructor(){ 22 | 23 | try { 24 | let rawFile = fs.readFileSync("./config/topology.json"); 25 | let top = JSON.parse(rawFile); 26 | this.networkDevices = top.networkDevices; 27 | } catch (e) { 28 | console.error("Error reading from file: ./config/topology.json"); 29 | } 30 | 31 | if(Topology.instance == null){ 32 | Topology.instance = this; 33 | } 34 | this.syncTopology = new SyncObject("topology"); 35 | 36 | this.networkDevices.forEach((dev)=>{ 37 | this.createInfrastructure(dev) 38 | }) 39 | this.updateInfrastructure(); 40 | 41 | WebsocketSyncServer.getInstance().addSyncObject("topology","global",this.syncTopology); 42 | } 43 | 44 | 45 | createInfrastructure(dev:NetworkAuth){ 46 | let int = new NetworkInfrastructureAbstrraction(dev, ()=>{this.updateInfrastructure()}) 47 | this.abstractions.push(int); 48 | } 49 | 50 | updateDevicesFromNmos(state:any){ 51 | this.nmosState = state; 52 | 53 | this.topologyState.devices = []; 54 | if(this.nmosState && this.nmosState.nodes){ 55 | for(let id in this.nmosState.nodes){ 56 | let node = this.nmosState.nodes[id]; 57 | let dev:NetworkDevice = { 58 | id:node.id, 59 | interfaces:[], 60 | signals:[], 61 | name:node.label, 62 | rendering:"", 63 | source:"nmos", 64 | type:"generic" 65 | } 66 | node.interfaces.forEach((inter)=>{ 67 | //let interface:NetworkInterface = { 68 | //name:inter.name, 69 | //mac:reduceMac(inter.port_id) 70 | //} 71 | }) 72 | this.topologyState.devices.push(dev); 73 | } 74 | this.syncTopology.setState(this.topologyState); 75 | } 76 | 77 | } 78 | 79 | 80 | 81 | updateInfrastructure(){ 82 | this.topologyState.infrastructure = []; 83 | this.abstractions.forEach((a)=>{ 84 | this.topologyState.infrastructure.push(a.device) 85 | }) 86 | this.syncTopology.setState(this.topologyState); 87 | } 88 | 89 | 90 | 91 | } 92 | 93 | interface TopologyState { 94 | devices:NetworkDevice[], 95 | infrastructure:NetworkInfrastructure[], 96 | }; 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /server/src/mediaDevices/imagineSnp.ts: -------------------------------------------------------------------------------- 1 | import { SyncObject } from "../lib/SyncServer/syncObject"; 2 | import { SyncLog } from "../lib/syncLog"; 3 | 4 | import { NmosRegistryConnector } from "../lib/nmosConnector"; 5 | import axios, { isAxiosError } from "axios"; 6 | import { writeFileSync } from "fs"; 7 | 8 | import { applyPatch, createPatch } from "rfc6902"; 9 | import { WebsocketClient } from "../lib/SyncServer/websocketClient"; 10 | import { WebsocketSyncServer } from "../lib/SyncServer/websocketSyncServer"; 11 | import { BehaviorSubject, Subject } from "rxjs"; 12 | import { MdnsService } from "../lib/mdnsService"; 13 | 14 | import { setTimeout as sleep } from 'node:timers/promises' 15 | 16 | 17 | const fs = require("fs"); 18 | const FormData = require('form-data'); 19 | const https = require("https"); 20 | 21 | 22 | 23 | 24 | export default class MediaDevImagineSnp { 25 | public static instance: MediaDevImagineSnp | null; 26 | 27 | public syncList: SyncObject; 28 | public quickState:Subject 29 | 30 | 31 | quickStateInternal = { 32 | label:"Imagine SNP", 33 | name:"", 34 | count:0, 35 | error:0, 36 | detail: [ 37 | 38 | ], 39 | note:"Hooks Only" 40 | } 41 | private state:any = { 42 | devices:{}, 43 | quickState:this.quickStateInternal, 44 | } 45 | 46 | 47 | 48 | constructor(settings:any){ 49 | this.quickState = new BehaviorSubject(this.quickStateInternal); 50 | // TODO Config of https ignore SSL cert error 51 | 52 | if(MediaDevImagineSnp.instance == null){ 53 | MediaDevImagineSnp.instance = this; 54 | } 55 | 56 | this.state["quickState"] = this.quickStateInternal; 57 | 58 | 59 | this.syncList = new SyncObject("mediadevimaginesnp"); 60 | this.syncList.setState(this.state); 61 | let server = WebsocketSyncServer.getInstance(); 62 | server.addSyncObject("mediadevimaginesnp","global",this.syncList); 63 | 64 | 65 | NmosRegistryConnector.registerModifier("receivers", (id,data)=>{ 66 | try{ 67 | data.label = data.label.replace("SNP-2121160083", "SNP") 68 | }catch(e){} 69 | return data; 70 | }) 71 | NmosRegistryConnector.registerModifier("senders", (id,data)=>{ 72 | // TODO, find serial and then replace 73 | try{ 74 | data.label = data.label.replace("SNP-2121160083", "SNP") 75 | }catch(e){} 76 | return data; 77 | }); 78 | 79 | } 80 | 81 | } 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /server/src/mediaDevices/riedelEmbrionix.ts: -------------------------------------------------------------------------------- 1 | import { SyncObject } from "../lib/SyncServer/syncObject"; 2 | import { SyncLog } from "../lib/syncLog"; 3 | 4 | import { NmosRegistryConnector } from "../lib/nmosConnector"; 5 | import axios, { isAxiosError } from "axios"; 6 | import { writeFileSync } from "fs"; 7 | 8 | import { applyPatch, createPatch } from "rfc6902"; 9 | import { WebsocketClient } from "../lib/SyncServer/websocketClient"; 10 | import { WebsocketSyncServer } from "../lib/SyncServer/websocketSyncServer"; 11 | import { BehaviorSubject, Subject } from "rxjs"; 12 | import { MdnsService } from "../lib/mdnsService"; 13 | 14 | import { setTimeout as sleep } from 'node:timers/promises' 15 | 16 | 17 | const fs = require("fs"); 18 | const FormData = require('form-data'); 19 | const https = require("https"); 20 | 21 | interface EMX_License { 22 | name:string, 23 | licensed:boolean 24 | } 25 | 26 | interface EMX_Device { 27 | id:string, 28 | ip:string[], 29 | name:string, 30 | type:string, 31 | mode:string, 32 | activeFirmware:string, 33 | license:EMX_License[], 34 | 35 | error:boolean, 36 | 37 | loading:boolean, 38 | reloadRequested:boolean 39 | }; 40 | 41 | export default class MediaDevRiedelEmbrionix { 42 | public static instance: MediaDevRiedelEmbrionix | null; 43 | 44 | public syncList: SyncObject; 45 | public quickState:Subject 46 | 47 | 48 | quickStateInternal = { 49 | label:"Riedel Embrionix Plattform", 50 | name:"riedelembrionix", 51 | count:0, 52 | error:0, 53 | detail: [ 54 | 55 | ], 56 | note:"FusioN, MuoN, VirtU" 57 | } 58 | private state:any = { 59 | devices:{}, 60 | quickState:this.quickStateInternal, 61 | } 62 | 63 | 64 | 65 | constructor(settings:any){ 66 | this.quickState = new BehaviorSubject(this.quickStateInternal); 67 | 68 | 69 | try{ 70 | if (!fs.existsSync("./state/mediadev_riedelembrionix")) { 71 | fs.mkdirSync("./state/mediadev_riedelembrionix"); 72 | console.log("Folder created: ./state/mediadev_riedelembrionix"); 73 | } 74 | }catch(e){ 75 | console.error("Error while creating Folder: ./state/mediadev_riedelembrionix"); 76 | } 77 | 78 | 79 | 80 | if(MediaDevRiedelEmbrionix.instance == null){ 81 | MediaDevRiedelEmbrionix.instance = this; 82 | } 83 | 84 | this.state["quickState"] = this.quickStateInternal; 85 | 86 | 87 | this.syncList = new SyncObject("mediadevriedelembrionix"); 88 | this.syncList.setState(this.state); 89 | let server = WebsocketSyncServer.getInstance(); 90 | server.addSyncObject("mediadevriedelembrionix","global",this.syncList); 91 | 92 | NmosRegistryConnector.registerModifier("nodes", (id,data)=>{ 93 | try{ 94 | // TODO config with filter and autodetect 95 | if(data.description == "st2110 node"){ 96 | this.updateDevice(data); 97 | } 98 | }catch(e){} 99 | return data; 100 | }); 101 | 102 | setInterval(()=>{ 103 | this.periodicReload(); 104 | },120000); 105 | } 106 | 107 | periodicReloadRunning = false; 108 | async periodicReload(){ 109 | if(this.periodicReloadRunning){ 110 | return; 111 | } 112 | 113 | this.periodicReloadRunning = true; 114 | for(let id of Object.keys(this.state.devices)){ 115 | //let emx = this.state.devices[id]; 116 | try{ 117 | await this.reloadDevice(id); 118 | }catch(e){} 119 | await sleep(1000); 120 | } 121 | 122 | this.quickStateInternal.note = "" 123 | this.periodicReloadRunning = false; 124 | } 125 | 126 | createrNewEMXDevice(id:string){ 127 | let emx:EMX_Device = { 128 | id:id, 129 | ip:[], 130 | name:"", 131 | type:"", 132 | mode:"", 133 | activeFirmware:"", 134 | license:[], 135 | 136 | error:false, 137 | 138 | loading:false, 139 | reloadRequested:false 140 | } 141 | return emx; 142 | } 143 | 144 | 145 | updateDevice(nmosData:any){ 146 | let ip = [] 147 | nmosData.api.endpoints.forEach((e)=>{ 148 | ip.push(e.host); 149 | }) 150 | if(this.state.devices.hasOwnProperty(nmosData.id)){ 151 | if(JSON.stringify(ip) == JSON.stringify(this.state.devices[nmosData.id].ip)){ 152 | this.state.devices[nmosData.id].name = nmosData.hostname; 153 | //Do nothing... 154 | return; 155 | }else{ 156 | this.state.devices[nmosData.id].ip = ip; 157 | this.state.devices[nmosData.id].name = nmosData.hostname; 158 | //update data... 159 | setTimeout(()=>{ 160 | this.reloadDevice(nmosData.id); 161 | },10) 162 | return; 163 | } 164 | 165 | } 166 | 167 | this.state.devices[nmosData.id] = this.createrNewEMXDevice(nmosData.id); 168 | this.state.devices[nmosData.id].ip = ip; 169 | this.state.devices[nmosData.id].name = nmosData.hostname; 170 | setTimeout(()=>{ 171 | this.reloadDevice(nmosData.id); 172 | },10) 173 | 174 | 175 | } 176 | 177 | async reloadDevice(id:string){ 178 | if(this.state.devices[id].loading == true){ 179 | this.state.devices[id].reloadRequested = true; 180 | return; 181 | } 182 | this.state.devices[id].loading = true; 183 | this.syncList.setState(this.state); 184 | 185 | let self = await this.apiRequest(this.state.devices[id].ip, "GET", "/self/information" ); 186 | if(self){ 187 | try{ 188 | let emx = this.state.devices[id]; 189 | emx.type = self.base_type; 190 | emx.mode = self.type; 191 | emx.activeFirmware = ""; 192 | 193 | emx.error = false; 194 | 195 | this.state.devices[id] = emx; 196 | }catch(e){ 197 | this.state.devices[id] = true; 198 | SyncLog.error("Embrionix", "failed to load data on: " + JSON.stringify(this.state.devices[id].ip) +" Message: "+ e.message) 199 | } 200 | } 201 | 202 | let firmware = await this.apiRequest(this.state.devices[id].ip, "GET", "/self/firmware" ); 203 | if(firmware){ 204 | 205 | try{ 206 | let emx = this.state.devices[id]; 207 | firmware.info.forEach((f)=>{ 208 | if(f.active){ 209 | emx.firmware = f.desc + " Ver: " + f.version 210 | } 211 | }) 212 | this.state.devices[id] = emx; 213 | 214 | }catch(e){ 215 | SyncLog.error("Embrionix", "failed to load firmware on: " + JSON.stringify(this.state.devices[id].ip) +" Message: "+ e.message) 216 | } 217 | } 218 | 219 | 220 | let license = await this.apiRequest(this.state.devices[id].ip, "GET", "/self/license" ); 221 | if(license){ 222 | try{ 223 | let emx = this.state.devices[id]; 224 | emx.license = []; 225 | Object.keys(license.feature).forEach((k)=>{ 226 | emx.license.push({ 227 | name:k, 228 | licensed: (license.feature[k] == "licensed" ? true: false) 229 | }) 230 | }) 231 | this.state.devices[id] = emx; 232 | 233 | }catch(e){ 234 | SyncLog.error("Embrionix", "failed to load license on: " + JSON.stringify(this.state.devices[id].ip) +" Message: "+ e.message) 235 | } 236 | 237 | } 238 | 239 | this.state.devices[id].loading = false; 240 | this.updateCount(); 241 | this.syncList.setState(this.state); 242 | if(this.state.devices[id].reloadRequested){ 243 | setTimeout(()=>{ 244 | this.reloadDevice(id); 245 | },0); 246 | } 247 | } 248 | 249 | updateCount(){ 250 | this.quickStateInternal.count = 0; 251 | this.quickStateInternal.error = 0; 252 | 253 | Object.values(this.state.devices).forEach((emx:EMX_Device)=>{ 254 | this.quickStateInternal.count ++; 255 | if(emx.error){ 256 | this.quickStateInternal.error ++; 257 | } 258 | }); 259 | this.state.quickState = this.quickStateInternal; 260 | this.quickState.next(this.quickStateInternal); 261 | } 262 | 263 | 264 | 265 | 266 | 267 | async apiRequest(ipList:string[],method:"POST"|"GET", href:string, data = null){ 268 | let baseUrl = ""; 269 | let ip = ""; 270 | 271 | for(let ipt of ipList){ 272 | baseUrl = "http://"+ipt+":80/emsfp/node/v1"; 273 | try{ 274 | let result = await axios.get(baseUrl, {timeout:30000}); 275 | ip = ipt; 276 | break; 277 | }catch(e){} 278 | } 279 | if(baseUrl == ""){ 280 | SyncLog.log("error","Embrionix", "No connection possible to: "+ ipList.join(", ")); 281 | return null; 282 | } 283 | 284 | let url = baseUrl + href; 285 | try{ 286 | let result:any = {}; 287 | if(method == "GET" ){ 288 | result = await axios.get(url, {}); 289 | }else if(method == "POST"){ 290 | result = await axios.post(url, data, { }); 291 | } 292 | return result.data; 293 | }catch(e){ 294 | SyncLog.log("error","Embrionix", "Can not access data on: "+ url + " Message: "+ e.message); 295 | } 296 | } 297 | 298 | 299 | 300 | } 301 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /server/src/networkDevices/aristaDCS7060XX2.ts: -------------------------------------------------------------------------------- 1 | import { NetworkAuth, NetworkInfrastructure } from "./networkDevice"; 2 | import { NetworkInfrastructureConnector } from "./networkInfrastructureConnector"; 3 | export class NIC_MOD extends NetworkInfrastructureConnector { 4 | 5 | 6 | device:NetworkInfrastructure = { 7 | id:"", 8 | name:"Arista DCS 7060 SX2", 9 | interfaces : [], 10 | rendering : {mode:"aristadcs"}, 11 | source : "config", 12 | type : "switch" 13 | }; 14 | updateInterval:number = 2000; 15 | 16 | counter = 0; 17 | name = ""; 18 | constructor(auth:NetworkAuth, changedCallback:any){ 19 | super(auth,changedCallback); 20 | 21 | this.device.name = auth.name; 22 | this.name = auth.name; 23 | 24 | let n = 0; 25 | 26 | for(let i = 1; i<49;i++){ 27 | this.device.interfaces.push({ 28 | attached:null, 29 | id:"E"+i, 30 | mac:"", 31 | name:"Eth "+ i, 32 | num:n++, 33 | speed:10000, 34 | maxspeed:25000, 35 | type:"sfp" 36 | }) 37 | } 38 | 39 | for(let i = 49; i<51;i++){ 40 | for(let j = 1; j<5; j++){ 41 | this.device.interfaces.push({ 42 | attached:null, 43 | id:"E"+i+"/"+j, 44 | mac:"", 45 | name:"Eth "+ i + "/"+j, 46 | num:n++, 47 | speed:100000, 48 | maxspeed:100000, 49 | type:"qsfp-split" 50 | }) 51 | } 52 | } 53 | 54 | for(let i = 51; i<55;i++){ 55 | this.device.interfaces.push({ 56 | attached:null, 57 | id:"E"+i, 58 | mac:"", 59 | name:"Eth "+ i, 60 | num:n++, 61 | speed:100000, 62 | maxspeed:100000, 63 | type:"qsfp" 64 | }) 65 | } 66 | 67 | this.sendUpdates() 68 | } 69 | 70 | 71 | sendUpdates(){ 72 | super.update(this.device); 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /server/src/networkDevices/dummySwitch.ts: -------------------------------------------------------------------------------- 1 | import { NetworkAuth, NetworkInfrastructure } from "./networkDevice"; 2 | import { NetworkInfrastructureConnector } from "./networkInfrastructureConnector"; 3 | export class NIC_MOD extends NetworkInfrastructureConnector { 4 | 5 | 6 | device:NetworkInfrastructure = { 7 | id:"", 8 | name:"dummySwitch_X", 9 | interfaces : [], 10 | rendering : null, 11 | source : "config", 12 | type : "switch" 13 | }; 14 | updateInterval:number = 2000; 15 | 16 | counter = 0; 17 | name = ""; 18 | constructor(auth:NetworkAuth, changedCallback:any){ 19 | super(auth,changedCallback); 20 | 21 | this.device.name = auth.name; 22 | this.name = auth.name; 23 | 24 | 25 | for(let i = 0; i<24;i++){ 26 | this.device.interfaces.push({ 27 | attached:null, 28 | id:""+i, 29 | mac:"", 30 | name:"int et "+ i, 31 | num:i, 32 | speed:10000, 33 | maxspeed:100000, 34 | type:"sfp" 35 | }) 36 | } 37 | 38 | this.sendUpdates() 39 | } 40 | 41 | 42 | sendUpdates(){ 43 | super.update(this.device); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /server/src/networkDevices/networkDevice.ts: -------------------------------------------------------------------------------- 1 | import { NetworkInfrastructureConnector } from "./networkInfrastructureConnector"; 2 | 3 | export class NetworkInfrastructureAbstrraction { 4 | settings:NetworkAuth; 5 | device:NetworkInfrastructure; 6 | 7 | updateCallback:any = null; 8 | 9 | connector:NetworkInfrastructureConnector | null = null; 10 | 11 | constructor( opt:NetworkAuth, callback:any){ 12 | this.settings = opt; 13 | this.updateCallback = callback; 14 | try{ 15 | let loaded = require("./"+this.settings.type); 16 | this.connector = new loaded.NIC_MOD(opt, (dev)=>{this.changed(dev)}); 17 | this.connector.changed = (dev)=>{this.changed(dev) ;}; 18 | }catch(e){ 19 | // TODO Logging 20 | //console.log(e) 21 | } 22 | 23 | } 24 | 25 | changed(dev:NetworkInfrastructure){ 26 | this.device = dev; 27 | if(this.updateCallback){ 28 | this.updateCallback(); 29 | } 30 | } 31 | } 32 | 33 | 34 | 35 | 36 | 37 | export interface NetworkAuth { 38 | name:string, 39 | type:string, 40 | connect:string, 41 | auth:string, 42 | }; 43 | 44 | export interface NetworkInfrastructure { 45 | type:"switch"|"router", 46 | name:string, 47 | source:"config"|"detected", 48 | id:string, 49 | interfaces:NetworkInterface[], 50 | rendering:any, 51 | } 52 | 53 | export interface NetworkDevice { 54 | type:"camera"|"converter"|"generic", 55 | name:string, 56 | source:"nmos", 57 | id:string, 58 | interfaces:NetworkInterface[], 59 | signals:NetworkDeviceSignal[], 60 | rendering:any, 61 | }; 62 | 63 | interface NetworkDeviceSignal { 64 | name:string 65 | } 66 | 67 | export interface NetworkInterface { 68 | name:string, 69 | id:string, 70 | num:number, 71 | speed:number, 72 | mac:string, 73 | type:"rj45"|"sfp"|"qsfp"|"qsfp-split", 74 | maxspeed:number, 75 | attached:{device:NetworkDevice,port:NetworkInterface}, 76 | } -------------------------------------------------------------------------------- /server/src/networkDevices/networkInfrastructureConnector.ts: -------------------------------------------------------------------------------- 1 | import { NetworkAuth, NetworkInfrastructure } from "./networkDevice"; 2 | 3 | export class NetworkInfrastructureConnector { 4 | changed:any = null; 5 | constructor(auth:NetworkAuth, changedCallback:any ){ 6 | this.changed = changedCallback; 7 | } 8 | update(device:NetworkInfrastructure){ 9 | if(this.changed){ 10 | this.changed(device); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | NMOS Crosspoint 3 | Copyright (C) 2021 Johannes Grieb 4 | */ 5 | 6 | 7 | const fs = require("fs"); 8 | 9 | import {MdnsService} from "./lib/mdnsService" 10 | 11 | import { SyncLog } from "./lib/syncLog"; 12 | 13 | 14 | import { NmosRegistryConnector } from "./lib/nmosConnector"; 15 | import { WebsocketClient } from "./lib/SyncServer/websocketClient"; 16 | 17 | import { WebsocketSyncServer } from "./lib/SyncServer/websocketSyncServer"; 18 | import { CrosspointAbstraction } from "./lib/crosspointAbstraction"; 19 | import { Topology } from "./lib/topology"; 20 | import { MediaDevices } from "./lib/mediaDevices"; 21 | import { SyncObject } from "./lib/SyncServer/syncObject"; 22 | import { parseSettings } from "./lib/parseSettings"; 23 | 24 | 25 | 26 | 27 | const uiConfig = { 28 | "disabledModules":{ 29 | "core":[] 30 | } 31 | }; 32 | 33 | 34 | const log = new SyncLog(); 35 | SyncLog.log("info", "Process", "Server Startup."); 36 | 37 | let settings: any = {}; 38 | try { 39 | let rawFile = fs.readFileSync("./config/settings.json"); 40 | let tempSettings = JSON.parse(rawFile); 41 | settings = parseSettings(tempSettings); 42 | } catch (e) { 43 | SyncLog.log("error", "Settings", "Error while reading file: ./config/settings.json", e); 44 | SyncLog.log("error", "Settings", "Can not run without Configuration..."); 45 | process.exit(); 46 | } 47 | 48 | if(settings.hasOwnProperty("logOutput")){ 49 | log.setOutput(settings.logOutput); 50 | } 51 | 52 | let serverPort = 80; 53 | let serverAddress = "0.0.0.0"; 54 | 55 | let modDisabled:string[]=[]; 56 | 57 | let mdns = new MdnsService(settings); 58 | try{ 59 | if(settings.hasOwnProperty("disabledModules") && settings.disabledModules.hasOwnProperty("core")){ 60 | uiConfig.disabledModules.core = settings.disabledModules.core; 61 | settings.core.forEach((m)=>{ 62 | let name = ""+m; 63 | modDisabled.push(name); 64 | }); 65 | } 66 | }catch(e){} 67 | 68 | try{ 69 | if(settings.hasOwnProperty('server') && settings.server.hasOwnProperty('port')){ 70 | let serverPortTemp = parseInt(settings.server.port); 71 | if(serverPortTemp > 0 && serverPortTemp < 65536){ 72 | serverPort = serverPortTemp; 73 | }else{ 74 | throw new Error("Settings server port not a usable number.") 75 | } 76 | }else{ 77 | throw new Error("Settings server port not a usable number.") 78 | } 79 | }catch(e){ 80 | SyncLog.log("error", "Settings", "Can not read Server Port from settings. Default to "+serverPort+".", e); 81 | } 82 | 83 | try{ 84 | if(settings.hasOwnProperty('server') && settings.server.hasOwnProperty('address')){ 85 | let serverAddressTemp = parseInt(settings.server.address); 86 | }else{ 87 | throw new Error("Settings server address not a usable."); 88 | } 89 | }catch(e){ 90 | SyncLog.log("error", "Settings", "Can not read Server Address from settings. Default to "+serverAddress+".", e); 91 | } 92 | 93 | WebsocketSyncServer.init(serverAddress, serverPort); 94 | let server = WebsocketSyncServer.getInstance(); 95 | let users:any = null; 96 | try { 97 | let rawFile = fs.readFileSync("./config/users.json"); 98 | users = JSON.parse(rawFile); 99 | } catch (e) { 100 | SyncLog.log("error", "Server", "Error while reading file: ./config/users.json", e); 101 | } 102 | if(users){ 103 | server.relaodAuthData(users); 104 | } 105 | 106 | 107 | // TODO.... load dynamic.... 108 | const mediaDevices = new MediaDevices(settings); 109 | 110 | const crosspoint = new CrosspointAbstraction(settings); 111 | const nmosConnector = new NmosRegistryConnector(settings); 112 | 113 | 114 | 115 | 116 | server.addSyncObject("log","global",log); 117 | 118 | server.addSyncObject("nmos","global",nmosConnector.syncNmos); 119 | server.addSyncObject("nmosConnectionState","global",nmosConnector.syncConnectionState); 120 | 121 | server.addSyncObject("crosspoint","global",crosspoint.syncCrosspoint); 122 | 123 | 124 | let topology = null; 125 | if(modDisabled.includes["topology"]){ 126 | SyncLog.info("server", "disabling module topology"); 127 | }else{ 128 | topology = new Topology(); 129 | } 130 | 131 | 132 | const uiConfigSync: SyncObject = new SyncObject("uiconfig", uiConfig); 133 | server.addSyncObject("uiconfig","public",uiConfigSync); 134 | 135 | 136 | 137 | 138 | 139 | server.addRoute("GET", "flowInfo","global" , (client: WebsocketClient, query:string[]) => { 140 | return new Promise((resolve, reject) => { 141 | let flowId = query[0]; 142 | if(flowId){ 143 | let flow = crosspoint.getFlowInfo(flowId); 144 | if(flow){ 145 | resolve({message:200, data:flow}); 146 | }else{ 147 | reject("flow not found"); 148 | } 149 | }else{ 150 | reject("missing flow Id"); 151 | } 152 | 153 | }); 154 | }); 155 | 156 | server.addRoute("POST", "makeconnection","global", (client: WebsocketClient, query:string[], postData: any) => { 157 | return new Promise((resolve, reject) => { 158 | crosspoint 159 | .makeConnection(postData) 160 | .then((data) => resolve({message:200, data:data})) 161 | .catch((m) => reject(m)); 162 | }); 163 | }); 164 | 165 | server.addRoute("POST", "changealias","global", (client: WebsocketClient, query:string[], postData: any) => { 166 | return new Promise((resolve, reject) => { 167 | crosspoint 168 | .changeAlias(postData.id, postData.alias) 169 | .then((m) => resolve(m)) 170 | .catch((m) => reject(m)); 171 | }); 172 | }); 173 | 174 | server.addRoute("POST", "enableFlow","global", (client: WebsocketClient, query:string[], postData: any) => { 175 | return new Promise((resolve, reject) => { 176 | crosspoint 177 | .enableFlow(postData.id, false) 178 | .then((m) => resolve(m)) 179 | .catch((m) => reject(m)); 180 | }); 181 | }); 182 | 183 | server.addRoute("POST", "disableFlow","global", (client: WebsocketClient, query:string[], postData: any) => { 184 | return new Promise((resolve, reject) => { 185 | crosspoint 186 | .enableFlow(postData.id, true) 187 | .then((m) => resolve(m)) 188 | .catch((m) => reject(m)); 189 | }); 190 | }); 191 | 192 | 193 | server.addRoute("POST", "setMulticast","global", (client: WebsocketClient, query:string[], postData: any) => { 194 | return new Promise((resolve, reject) => { 195 | crosspoint 196 | .setMulticast(postData.id, postData.data) 197 | .then((m) => resolve(m)) 198 | .catch((m) => reject(m)); 199 | }); 200 | }); 201 | 202 | 203 | 204 | 205 | 206 | server.addRoute("POST", "togglehidden","global", (client: WebsocketClient, query:string[], postData: any) => { 207 | return new Promise((resolve, reject) => { 208 | crosspoint 209 | .toggleHidden(postData.id) 210 | .then((m) => resolve(m)) 211 | .catch((m) => reject(m)); 212 | }); 213 | }); 214 | 215 | 216 | 217 | // Crosspoint editor 218 | server.addRoute("POST", "crosspoint","global", (client: WebsocketClient, query:string[], postData: any) => { 219 | return new Promise((resolve, reject) => { 220 | crosspoint 221 | .crosspointApi(postData) 222 | .then((m) => resolve(m)) 223 | .catch((m) => reject(m)); 224 | }); 225 | }); 226 | 227 | 228 | -------------------------------------------------------------------------------- /server/src/service.ts: -------------------------------------------------------------------------------- 1 | const { spawn } = require('node:child_process'); 2 | 3 | 4 | // TODO service file 5 | 6 | let args = process.argv.slice(2); 7 | let serverProcess:any = null; 8 | function runServer(){ 9 | serverProcess = spawn(__dirname+"/server.js",args,{}); 10 | serverProcess.on("error", (err)=>{ 11 | console.error("Service Error: ", err); 12 | }); 13 | serverProcess.on("data", (data)=>{ 14 | console.log("Service Data: ", data); 15 | }); 16 | 17 | } 18 | 19 | runServer(); 20 | 21 | 22 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "sourceMap": true, 8 | "lib": ["esnext"] 9 | } 10 | } -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Svelte + TS + Vite 2 | 3 | This template should help get you started developing with Svelte and TypeScript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). 8 | 9 | ## Need an official Svelte framework? 10 | 11 | Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. 12 | 13 | ## Technical considerations 14 | 15 | **Why use this over SvelteKit?** 16 | 17 | - It brings its own routing solution which might not be preferable for some users. 18 | - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. 19 | 20 | This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. 21 | 22 | Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. 23 | 24 | **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** 25 | 26 | Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. 27 | 28 | **Why include `.vscode/extensions.json`?** 29 | 30 | Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. 31 | 32 | **Why enable `allowJs` in the TS template?** 33 | 34 | While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. 35 | 36 | **Why is HMR not preserving my local component state?** 37 | 38 | HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). 39 | 40 | If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. 41 | 42 | ```ts 43 | // store.ts 44 | // An extremely simple external store 45 | import { writable } from 'svelte/store' 46 | export default writable(0) 47 | ``` 48 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NMOS Crosspoint 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nmos_crosspoint_ui", 3 | "version": "2.0", 4 | "author": "Johannes Grieb", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite --host 0.0.0.0", 9 | "build": "vite build", 10 | "preview": "vite preview", 11 | "check": "svelte-check --tsconfig ./tsconfig.json" 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/vite-plugin-svelte": "^3.0.1", 15 | "@tsconfig/svelte": "^5.0.2", 16 | "sass": "^1.70.0", 17 | "svelte": "^4.2.8", 18 | "svelte-check": "^3.6.2", 19 | "svelte-hero-icons": "^5.1.0", 20 | "tslib": "^2.6.2", 21 | "typescript": "^5.2.2", 22 | "vite": "^5.4.7" 23 | }, 24 | "dependencies": { 25 | "@fontsource-variable/roboto-mono": "^5.0.18", 26 | "@fontsource/roboto": "^5.0.12", 27 | "@xyflow/svelte": "^0.0.35", 28 | "autoprefixer": "^10.4.17", 29 | "daisyui": "^4.6.1", 30 | "happy-svelte-scrollbar": "^1.1.0", 31 | "heroicons": "^2.1.1", 32 | "install": "^0.13.0", 33 | "js-sha256": "^0.11.0", 34 | "npm": "^10.4.0", 35 | "perfect-scrollbar": "^1.5.5", 36 | "postcss": "^8.4.33", 37 | "rfc6902": "^5.1.1", 38 | "rxjs": "^7.8.1", 39 | "svelte-json-tree": "^2.2.0", 40 | "svelte-routing": "^2.11.0", 41 | "svrollbar": "^0.12.0", 42 | "tailwindcss": "^3.4.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss'), require('autoprefixer')], 3 | }; 4 | -------------------------------------------------------------------------------- /ui/public/assets/connectionWorker.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var pass = ""; 4 | 5 | self.addEventListener("install", (event) => { 6 | console.log("install"); 7 | }); 8 | self.addEventListener("activate", (event) => { 9 | console.log("activate"); 10 | }); 11 | 12 | self.addEventListener("message", function(event) { 13 | 14 | console.log(event) 15 | 16 | var data = JSON.parse(event.data); 17 | if(data.hasOwnProperty("__getPass")){ 18 | event.source.postMessage(JSON.stringify({__pass:pass})); 19 | console.log("get Pass: ") 20 | } 21 | 22 | if(data.hasOwnProperty("__setPass")){ 23 | pass = data.__setPass 24 | 25 | console.log("set Pass: ") 26 | } 27 | 28 | //port.addEventListener("message", function(e) { 29 | //var data = JSON.parse(e.data); 30 | //if(data.hasOwnProperty("__getPass")){ 31 | // port.postMessage(JSON.stringify({__pass:pass})); 32 | // console.log("get Pass: "+pass) 33 | // 34 | //} 35 | //if(data.hasOwnProperty("__setPass")){ 36 | // pass = data.__setPass 37 | // console.log("set Pass: "+pass) 38 | //} 39 | 40 | //}, false); 41 | //// 42 | //port.start(); 43 | }, false); -------------------------------------------------------------------------------- /ui/public/crosspoint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/safelist.txt: -------------------------------------------------------------------------------- 1 | bg-red-600 2 | bg-red-300 3 | text-white 4 | bg-green-500 5 | bg-blue-500 6 | bg-base-100 7 | bg-base-200 8 | bg-base-300 9 | bg-base-400 10 | bg-base-500 11 | bg-base-600 12 | 13 | swap 14 | swap-rotate 15 | theme-controller 16 | swap-on 17 | fill-current 18 | w-10 19 | h-10 20 | swap-off 21 | w-6 22 | h-6 23 | toggle 24 | 25 | loading 26 | loading-ring 27 | loading-lg 28 | glass 29 | text-error 30 | text-info 31 | text-warning 32 | text-success 33 | 34 | badge-error 35 | badge-success 36 | 37 | input 38 | input-ghost 39 | 40 | tooltip 41 | dropdown 42 | 43 | progress 44 | toast 45 | toast-* 46 | alert 47 | alert-error 48 | alert-info 49 | alert-warning 50 | alert-success 51 | 52 | pt-8 53 | pt-4 54 | pb-8 -------------------------------------------------------------------------------- /ui/src/App.svelte: -------------------------------------------------------------------------------- 1 | 178 | 179 | 180 |
181 | 182 |
183 | {updateGlobalTake(e)}}> 184 | {updateGlobalTake(e)}}> 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 |

404 : Not found

201 | 202 | 203 | 204 | 205 |
206 | 285 |
286 |
287 | 288 | 289 | 290 | 291 | 292 | -------------------------------------------------------------------------------- /ui/src/app.scss: -------------------------------------------------------------------------------- 1 | @import '@fontsource/roboto/400.css'; 2 | @import '@fontsource/roboto/500.css'; 3 | 4 | @import '@fontsource/roboto/700.css'; 5 | 6 | @import '@fontsource-variable/roboto-mono/wght.css'; 7 | 8 | 9 | 10 | 11 | @tailwind base; 12 | @tailwind components; 13 | @tailwind utilities; 14 | 15 | 16 | @import './scss/components.scss'; 17 | @import './scss/components/scrollbars.scss'; 18 | @import './scss/components/prettyjson.scss'; 19 | @import './scss/components/datatable.scss'; 20 | @import './scss/components/input.scss'; 21 | 22 | @import './scss/layout.scss'; 23 | @import './scss/crosspoint.scss'; 24 | @import './scss/topology.scss'; 25 | @import './scss/details.scss'; 26 | 27 | @import './scss/debug.scss'; 28 | @import './scss/setup.scss'; -------------------------------------------------------------------------------- /ui/src/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /ui/src/lib/InlineEditor.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 | 53 | 54 |
{click();}}> 55 | {label + (active?"":value)} 56 | 57 | { update(e);}} 59 | on:focus={(e)=>{ focus();}} 60 | /> 61 | 62 |
-------------------------------------------------------------------------------- /ui/src/lib/OverlayMenu/OverlayMenu.svelte: -------------------------------------------------------------------------------- 1 | 57 | 58 | 59 |
60 | {#each menuList as menu} 61 |
62 | 67 |
68 | {/each} 69 |
70 | 71 | {#if tooltip.active} 72 |
73 | {tooltip.text} 74 |
75 | {/if} 76 | -------------------------------------------------------------------------------- /ui/src/lib/OverlayMenu/OverlayMenuService.ts: -------------------------------------------------------------------------------- 1 | import { ReplaySubject, Observable, Subject, BehaviorSubject } from 'rxjs'; 2 | import { TestTools } from 'rxjs/internal/util/Immediate'; 3 | 4 | 5 | class _OverlayMenuService { 6 | openMenus:any[] = []; 7 | menuObservable:Subject; 8 | tooltipObservable:Subject; 9 | tooltipData = { 10 | active:false, 11 | uipos:{y:0,x:0}, 12 | text:"" 13 | } 14 | constructor (){ 15 | this.menuObservable = new Subject(); 16 | this.menuObservable.next([]) 17 | this.tooltipObservable = new Subject(); 18 | this.tooltipObservable.next( { 19 | active:false, 20 | uipos:{y:0,x:0}, 21 | text:"" 22 | }); 23 | } 24 | 25 | open(menu:any, event:any = null){ 26 | this.openMenus = [] 27 | menu["pos"] = {x:0,y:0} 28 | if(event){ 29 | event.stopPropagation(); 30 | menu["pos"] = {x:0,y:0} 31 | menu["pos"]["x"] = event.x; 32 | menu["pos"]["y"] = event.y; 33 | } 34 | 35 | this.openMenus.push(menu); 36 | this.menuObservable.next(this.openMenus); 37 | } 38 | close(index:number){ 39 | this.openMenus.splice(index,1); 40 | this.menuObservable.next(this.openMenus); 41 | } 42 | closeAll(){ 43 | this.openMenus = []; 44 | this.menuObservable.next(this.openMenus); 45 | 46 | } 47 | 48 | 49 | tooltip(element:any){ 50 | 51 | 52 | let position:string = element.getAttribute("data-tooltip-position"); 53 | let p = ["right","bottom"]; 54 | try{ 55 | if(position){ 56 | p = position.split(","); 57 | } 58 | }catch(e){} 59 | 60 | element.addEventListener("mouseover", (event:any) => { 61 | let text:string = element.getAttribute("data-tooltip"); 62 | let r = element.getBoundingClientRect(); 63 | let y = 0; 64 | let x = 0; 65 | 66 | let mx = 0; 67 | let my = 0; 68 | 69 | 70 | if( p[0] == "right"){ 71 | x = r.x+r.width; 72 | mx = 0; 73 | } 74 | if( p[0] == "center"){ 75 | x = r.x + r.width/2; 76 | mx = 50; 77 | } 78 | if( p[0] == "left"){ 79 | x = r.x; 80 | mx = 100; 81 | } 82 | 83 | 84 | if( p[1] == "top"){ 85 | y = r.y; 86 | my = 100; 87 | } 88 | if( p[1] == "middle"){ 89 | y = r.y + r.height/2; 90 | my = 50; 91 | } 92 | if( p[1] == "bottom"){ 93 | y = r.y + r.height; 94 | my = 0; 95 | } 96 | 97 | this.tooltipObservable.next({ 98 | active:true, 99 | uipos:{ 100 | x:x,y:y, 101 | mx:mx, 102 | my:my, 103 | }, 104 | text:text 105 | }) 106 | }) 107 | element.addEventListener("mouseout", (ev:any) => { 108 | this.tooltipObservable.next({ 109 | active:false, 110 | uipos:{y:0,x:0}, 111 | text:"" 112 | }) 113 | }) 114 | } 115 | 116 | }; 117 | 118 | const OverlayMenuService: _OverlayMenuService = new _OverlayMenuService(); 119 | export default OverlayMenuService; 120 | 121 | -------------------------------------------------------------------------------- /ui/src/lib/PrettyJson.svelte: -------------------------------------------------------------------------------- 1 | 56 | 57 | 58 |
59 | 60 |
{@html content}
61 | 62 |
-------------------------------------------------------------------------------- /ui/src/lib/ScrollArea.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 |
36 | 37 |
38 | 39 | -------------------------------------------------------------------------------- /ui/src/lib/ServerConnector/ServerConnectorOverlay.svelte: -------------------------------------------------------------------------------- 1 | 62 | 63 | 64 | 65 | 66 | 67 | 99 | 100 | 101 | 102 | 103 |
104 | {#each feedbackList as feedback} 105 | {#if !feedback.hidden } 106 |
{click(feedback);}}> 107 | {feedback.message} 108 | 109 | {#if feedback.data.type == "connection"} 110 | Flows connectd: {feedback.data.result.success} 111 | {#if feedback.data.result.disconnect } 112 | Flows disconnected: {feedback.data.result.disconnect} 113 | {/if} 114 | {#if feedback.data.result.failed } 115 | Flows failed: {feedback.data.result.failed} 116 | 117 | {#each feedback.data.result.reasons as r} 118 | {r} 119 | {/each} 120 | 121 | {/if} 122 | {/if} 123 |
124 | {/if} 125 | {/each} 126 |
127 | 128 | 129 | {#if loading } 130 |
131 | 132 |
133 | {/if} 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /ui/src/lib/SetupDevice.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | {#if source == "nmosgrp"} 32 |

Setup Device ( {deviceId} )

33 | 34 | {#if loading} 35 |
Reload
36 |
Loading
37 | {:else} 38 | 39 | {/if} 40 | 41 | {/if} 42 | 43 | -------------------------------------------------------------------------------- /ui/src/lib/SetupFlow.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | {#if source == "nmos"} 40 |

Setup Flow ( {flowId} )

41 | 42 | {#if loading} 43 |
Loading
44 | {:else} 45 |
Reload
46 |
47 | Node, Device, Group, Sender 48 |
49 | 50 |
51 | Edit Label 52 |
53 | 54 |
55 | Change Multicast Config 56 |
57 | 58 |
59 | Activate 60 |
61 | 62 |
63 | Network Interfaces, 64 | Bitrates 65 |
66 | 67 |
68 |
69 |             {sdpFile}        
70 |             
71 |
72 | {/if} 73 | 74 | {/if} 75 | 76 | -------------------------------------------------------------------------------- /ui/src/lib/SvelteFlowNodes/DeviceNode.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 | Device Node 14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /ui/src/lib/SvelteFlowNodes/SwitchNode.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 |
35 | 36 |
37 | {#each data.interfaces as int} 38 | {#if isTopInterface(int.id)} 39 |
40 | 41 | {int.id} 42 |
43 | {/if} 44 | {/each} 45 |
46 |
{data.name}
47 |
48 | {#each data.interfaces as int} 49 | {#if !isTopInterface(int.id)} 50 |
51 | 52 | {int.id} 53 |
54 | {/if} 55 | {/each} 56 |
57 | 58 |
59 | -------------------------------------------------------------------------------- /ui/src/lib/functions.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | export function getSearchTokens(search:string){ 6 | let parts = search.split("||"); 7 | let tokens:string[][] = []; 8 | parts.forEach((p)=>{ 9 | let combT = p.split("&&") 10 | let comb:string[] = [] 11 | combT.forEach((c)=>{ 12 | if(c != ""){ 13 | comb.push(c.trim()) 14 | } 15 | }) 16 | if(comb.length != 0){ 17 | tokens.push(comb); 18 | } 19 | }) 20 | 21 | return tokens; 22 | } 23 | 24 | 25 | export function tokenSearch(input:string|any, tokens:string[][], keys:string[]|null = null){ 26 | if(tokens.length == 0){ 27 | return true; 28 | } 29 | if(keys){ 30 | 31 | }else{ 32 | input = { "text" : input }; 33 | keys = ["text"]; 34 | } 35 | 36 | let found = false; 37 | 38 | 39 | 40 | tokens.forEach((token:string[])=>{ 41 | let combFound = true; 42 | token.forEach((comb)=>{ 43 | let keyFound = false; 44 | keys.forEach((k)=>{ 45 | if(input[k].search(new RegExp(comb, "i")) != -1){ 46 | keyFound = true 47 | } 48 | }); 49 | if(!keyFound){ 50 | combFound = false; 51 | } 52 | }); 53 | 54 | if(combFound){ 55 | found = true; 56 | } 57 | }); 58 | 59 | return found; 60 | } -------------------------------------------------------------------------------- /ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import './app.scss' 2 | import App from './App.svelte' 3 | 4 | const app = new App({ 5 | target: document.getElementById('app'), 6 | }) 7 | 8 | export default app 9 | -------------------------------------------------------------------------------- /ui/src/routes/debug.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 | 53 |
54 |
55 | 81 |
82 |
83 |
102 |
-------------------------------------------------------------------------------- /ui/src/routes/devices.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | {#each state.deviceTypes as dev} 46 | { 47 | document.location.href = "/mediadevices/"+ dev.name; 48 | }}> 49 | 52 | 53 | 56 | 57 | 62 | 63 | 64 | {#each dev.detail as detail} 65 | 68 | {/each} 69 | 70 | 71 | {/each} 72 | 73 | 74 |
35 | Device Type 36 | 39 | Count 40 |
50 | {dev.label} 51 | 54 | Total: {dev.count} 55 | 58 | {#if dev.error > 0} 59 | Error: {dev.error} 60 | {/if} 61 | 66 | {detail.label}: {detail.count} 67 | {dev.note}
75 |
76 | 77 | 78 |
79 | -------------------------------------------------------------------------------- /ui/src/routes/logging.svelte: -------------------------------------------------------------------------------- 1 | 240 | 241 | 242 |
243 | 244 | 245 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | {#each tableCols as col} 309 | 310 | {/each} 311 | 312 | 313 | 314 | {#each uiList as log} 315 | {toggleExpand(log.id)}} class="log-line {(isNew(log.id) ? "log-line-new":"")}"> 316 | {#if log.raw} 317 | 318 | {:else} 319 | 320 | {/if} 321 | {#each tableCols as col} 322 | 359 | 360 | {/each} 361 | 362 | {#if isExpanded(log.id)} 363 | 364 | 365 | 366 | 373 | 374 | {/if} 375 | {/each} 376 | 377 | 378 |
{col.name}
{toggleExpand(log.id)}}> 323 | 324 | {#if col.id == "time"} 325 | {renderTime(log.time)} 326 | {/if} 327 | 328 | {#if col.id == "severity"} 329 | {#if log.severity == "error"} 330 |
Error
331 | {/if} 332 | 333 | {#if log.severity == "warning"} 334 |
Warning
335 | {/if} 336 | 337 | {#if log.severity == "info"} 338 |
Info
339 | {/if} 340 | 341 | {#if log.severity == "verbose"} 342 |
Verbose
343 | {/if} 344 | 345 | {#if log.severity == "debug"} 346 |
Debug
347 | {/if} 348 | 349 | {/if} 350 | 351 | {#if col.id == "topic"} 352 | {log.topic} 353 | {/if} 354 | 355 | {#if col.id == "text"} 356 | {log.text} 357 | {/if} 358 |
367 | {#if log.raw} 368 | 369 | {:else} 370 | No data available 371 | {/if} 372 |
379 |
380 | 381 | 382 |
-------------------------------------------------------------------------------- /ui/src/routes/mediadevices/riedelembrionix.svelte: -------------------------------------------------------------------------------- 1 | 298 | 299 | 300 | 301 |
302 | 303 | 304 | 305 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | {#each tableCols as col} 333 | {#if !filter.hiddenCols.includes(col.id)} 334 | 364 | {/if} 365 | {/each} 366 | 367 | 368 | 369 | {#each list as dev} 370 | 371 | {#each tableCols as col} 372 | {#if !filter.hiddenCols.includes(col.id)} 373 | 416 | {/if} 417 | {/each} 418 | 419 | 420 | {/each} 421 | 422 | 423 |
335 |
336 |
{col.name}
337 |
338 | {#if col.sortable} 339 | {#if filter.sortCols.includes(col.id + "__down")} 340 | 343 | {:else if filter.sortCols.includes(col.id + "__up")} 344 | 347 | {:else} 348 | 351 | {/if} 352 | {/if} 353 | 354 |
355 |
356 | {#if col.resize} 357 |
{startResizeDrag(e,col.id)}} 359 | on:drag={(e)=>{updateResizeDrag(e,col.id)}} 360 | on:dragend={(e)=>{endResizeDrag(e,col.id)}} 361 | >
362 | {/if} 363 |
374 | 375 | {#if col.id == "state"} 376 |
377 | {/if} 378 | 379 | 380 | 381 | 382 | {#if col.id == "name"} 383 | {dev.name} 384 | {/if} 385 | 386 | {#if col.id == "type"} 387 | {dev.type} 388 | {/if} 389 | 390 | {#if col.id == "mode"} 391 | {dev.mode} 392 | {/if} 393 | 394 | {#if col.id == "ip"} 395 | {#each dev.ip as ip} 396 | {ip}
397 | {/each} 398 | {/if} 399 | 400 | {#if col.id == "firmware"} 401 | {dev.firmware} 402 | {/if} 403 | 404 | {#if col.id == "license"} 405 | {#each dev.license as lic } 406 | {#if lic.licensed } 407 |
  {lic.name}

408 | {/if} 409 | {/each} 410 | {/if} 411 | 412 | 413 | 414 | 415 |
424 |
425 | 426 | 427 |
428 | 429 | 430 | 431 | 432 | 433 | 452 | 453 | -------------------------------------------------------------------------------- /ui/src/routes/setup.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 |
34 | 42 |
43 |
44 |
63 |
-------------------------------------------------------------------------------- /ui/src/routes/topology.svelte: -------------------------------------------------------------------------------- 1 | 70 | 71 | 72 |
73 | console.log('on node click', event.detail.node)} 80 | > 81 | 82 | 83 | 84 |
-------------------------------------------------------------------------------- /ui/src/scss/components.scss: -------------------------------------------------------------------------------- 1 | 2 | .btn { 3 | svg {width:24px;} 4 | } 5 | 6 | .icon-large { svg {width:20px;}} 7 | .icon-large-audio { svg {width:20px; stroke:theme('colors.amber.600')}} 8 | .icon-large-video { svg {width:20px; stroke:theme('colors.green.600')}} 9 | .icon-large-data { svg {width:20px; stroke:theme('colors.blue.600')}} 10 | 11 | label svg { width:20px;} 12 | 13 | 14 | .input { 15 | &:focus-within { outline:none;} 16 | input { 17 | &:focus {outline: none;} 18 | 19 | } 20 | } 21 | 22 | .filter-nav { 23 | .input { 24 | height: unset; 25 | padding-top:0.4rem; 26 | padding-bottom:0.4rem; 27 | &:focus-within { border:none; outline:none;} 28 | input { 29 | background-color:transparent; 30 | &:focus {outline: none;} 31 | 32 | } 33 | } 34 | } 35 | 36 | 37 | .content-container { 38 | height:100%; 39 | width:100%; 40 | display:flex; flex-direction:column; 41 | position:relative; 42 | 43 | 44 | .filter-nav { 45 | margin: 0.5rem; 46 | 47 | .btn-nav { 48 | @apply btn; 49 | @apply btn-circle; 50 | @apply btn-sm; 51 | 52 | padding:3px; 53 | 54 | &:hover { 55 | outline:none; 56 | border:none; 57 | } 58 | } 59 | .nav-spacer { 60 | background-color:transparent; 61 | flex-grow:2; 62 | } 63 | } 64 | 65 | 66 | 67 | 68 | 69 | } 70 | 71 | 72 | 73 | .overlay-server-loading { 74 | overflow:hidden; 75 | position:absolute; 76 | z-index : 1000; 77 | bottom:0px; left:0px; right:0px; 78 | height:20px; 79 | 80 | .progress { 81 | width:100%; 82 | } 83 | } 84 | 85 | 86 | 87 | .overlay-menu-container { 88 | .overlay-menu { 89 | position:absolute; 90 | z-index : 1000; 91 | ul { 92 | background-color: theme('colors.slate.600'); 93 | @apply menu p-2 shadow rounded-box w-52; 94 | 95 | } 96 | 97 | } 98 | } 99 | 100 | .overlay-tooltip { 101 | position:absolute; 102 | z-index : 999; 103 | span { 104 | @apply menu shadow; 105 | background-color: theme('colors.slate.700'); 106 | color:theme('colors.neutral-100'); 107 | padding:2px 8px; 108 | border-radius:4px; 109 | } 110 | } 111 | 112 | 113 | 114 | 115 | .log-line { 116 | animation: background-color 0.5s ease-in-out ; 117 | &.log-line-new { 118 | background-color: theme('colors.neutral-700') !important; 119 | } 120 | } 121 | 122 | 123 | .tooltip { 124 | &:before { 125 | z-index:500; 126 | } 127 | &:after { 128 | z-index:500; 129 | } 130 | } 131 | 132 | 133 | .overlay-server-feedback { 134 | &.toast { 135 | z-index:900; 136 | inset-inline-end: 120px !important; 137 | .alert { 138 | display:flex; 139 | flex-direction:column; 140 | align-items: start; 141 | gap:8px; 142 | 143 | 144 | .alert-title { 145 | font-weight: 600; 146 | } 147 | .alert-detail { 148 | font-size: 14px; 149 | } 150 | } 151 | } 152 | } 153 | 154 | 155 | 156 | 157 | 158 | html[data-theme="light"] { 159 | .overlay-menu-container { 160 | .overlay-menu { 161 | ul { 162 | background-color: theme('colors.slate.300'); 163 | } 164 | } 165 | } 166 | 167 | .overlay-tooltip { 168 | span { 169 | background-color: theme('colors.neutral-700'); 170 | color:theme('colors.neutral-100'); 171 | } 172 | } 173 | 174 | 175 | } 176 | -------------------------------------------------------------------------------- /ui/src/scss/components/datatable.scss: -------------------------------------------------------------------------------- 1 | .data-table { 2 | min-width: 100%; 3 | 4 | thead { 5 | padding:16px 0px 0px 0px; 6 | position: sticky; 7 | top: 0px; 8 | z-index: 5; 9 | height:50px; 10 | } 11 | 12 | td { 13 | padding:12px 16px; 14 | position:relative; 15 | vertical-align: top; 16 | line-height: 22px; 17 | .table-cell {display:flex; 18 | width:100%; 19 | height:100%; 20 | flex-direction: row; 21 | .table-content { 22 | flex-grow:2 23 | } 24 | } 25 | span { 26 | text-wrap: nowrap; 27 | } 28 | } 29 | 30 | td.data-table-fixed-col { 31 | position: sticky; 32 | left:0; 33 | } 34 | 35 | thead { 36 | td.data-table-fixed-col { 37 | z-index:10 38 | } 39 | } 40 | 41 | tbody { 42 | td.data-table-fixed-col { 43 | z-index:4 44 | } 45 | } 46 | 47 | .btn { 48 | @apply btn-sm; 49 | padding:5px; 50 | margin:-5px 0px -8px 0px; 51 | svg { 52 | width:20px; 53 | height: 20px; 54 | } 55 | 56 | &.btn-hover { 57 | visibility: hidden; 58 | position:absolute; 59 | top:8px; right:8px; 60 | } 61 | } 62 | .badge { 63 | top: 2px; 64 | position: relative; 65 | } 66 | 67 | td:hover { 68 | .btn-hover { 69 | visibility: visible; 70 | } 71 | } 72 | 73 | .btn-action { 74 | 75 | } 76 | 77 | .btn-more { 78 | 79 | } 80 | 81 | thead { 82 | tr { 83 | background-color: theme('colors.base-100'); 84 | td.data-table-fixed-col { 85 | background-color: theme('colors.base-100'); 86 | } 87 | } 88 | 89 | .sort-button { 90 | 91 | } 92 | 93 | 94 | .resize-handler { 95 | 96 | background-color: theme('colors.slate.500'); 97 | width:3px; 98 | position: absolute; 99 | right: 0px; 100 | top: 8px; 101 | height:24px; 102 | cursor:e-resize; 103 | 104 | } 105 | } 106 | 107 | tbody{ 108 | tr:nth-child(even) { 109 | background-color: theme('colors.base-200'); 110 | td.data-table-fixed-col { 111 | background-color: theme('colors.base-200'); 112 | } 113 | &.data-table-expanded { 114 | background-color: theme('colors.base-300'); 115 | td.data-table-fixed-col { 116 | background-color: theme('colors.base-300'); 117 | } 118 | } 119 | } 120 | tr:nth-child(odd) { 121 | background-color: theme('colors.base-100'); 122 | td.data-table-fixed-col { 123 | background-color: theme('colors.base-100'); 124 | } 125 | &.data-table-expanded { 126 | background-color: theme('colors.base-300'); 127 | td.data-table-fixed-col { 128 | background-color: theme('colors.base-300'); 129 | } 130 | } 131 | } 132 | tr:hover { 133 | background-color: theme('colors.slate.700'); 134 | td.data-table-fixed-col { 135 | background-color: theme('colors.slate.700'); 136 | } 137 | &.data-table-expanded { 138 | background-color: theme('colors.slate.700'); 139 | td.data-table-fixed-col { 140 | background-color: theme('colors.slate.700'); 141 | } 142 | } 143 | }} 144 | 145 | .data-table-expand { 146 | display:inline-block; 147 | 148 | width:24px; 149 | svg { 150 | width:20px; 151 | margin-top:2px; 152 | height:20px; 153 | } 154 | 155 | &.data-table-expand-active { 156 | svg { 157 | transform:rotate(90deg) 158 | } 159 | } 160 | } 161 | 162 | 163 | 164 | .data-table-action-buttons { 165 | display:flex; 166 | flex-direction:row; 167 | button {margin-right:10px;} 168 | } 169 | 170 | .data-table-flex-row { 171 | display:flex; 172 | flex-direction:row; 173 | div {margin-right:5px;} 174 | } 175 | 176 | 177 | 178 | 179 | 180 | } 181 | 182 | html[data-theme="light"] { 183 | .data-table { 184 | 185 | 186 | tbody{ 187 | tr:hover { 188 | background-color: theme('colors.slate.300'); 189 | td.data-table-fixed-col { 190 | background-color: theme('colors.slate.300'); 191 | } 192 | &.data-table-expanded { 193 | background-color: theme('colors.slate.300'); 194 | } 195 | } 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /ui/src/scss/components/input.scss: -------------------------------------------------------------------------------- 1 | .inline-editor { 2 | display:flex; 3 | flex-direction: row; 4 | padding:2px 4px; 5 | 6 | &.active { 7 | border:1px solid theme('colors.info');; 8 | border-radius:2px; 9 | } 10 | .inline-editor-label { 11 | flex:0; 12 | } 13 | .inline-editor-value { 14 | flex:2; 15 | } 16 | 17 | .inline-editor-input { 18 | width:100%; 19 | -webkit-appearance: none; 20 | -moz-appearance: none; 21 | appearance: none; 22 | 23 | &:focus, &:focus{ 24 | outline: none; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /ui/src/scss/components/prettyjson.scss: -------------------------------------------------------------------------------- 1 | .pretty-json { 2 | span {text-wrap: wrap !important} 3 | pre { 4 | border: 1px solid silver; 5 | padding: 10px 20px; 6 | } 7 | .json-key { 8 | color: brown; 9 | } 10 | .json-value { 11 | color: navy; 12 | } 13 | .json-string { 14 | color: olive; 15 | } 16 | } -------------------------------------------------------------------------------- /ui/src/scss/components/scrollbars.scss: -------------------------------------------------------------------------------- 1 | 2 | body:has(.content-container-scroll:hover) { 3 | overflow-x: hidden; 4 | overscroll-behavior-x: contain; 5 | } 6 | 7 | .content-container-scroll--back { 8 | overflow:scroll; 9 | overscroll-behavior-x: contain; 10 | } 11 | 12 | .content-container-scroll { 13 | 14 | 15 | overflow:scroll; 16 | -ms-overflow-style: none; /* IE and Edge */ 17 | scrollbar-width: none; /* Firefox */ 18 | &::-webkit-scrollbar { 19 | display: none; 20 | } 21 | } 22 | 23 | .scrollbars { 24 | pointer-events:none; 25 | opacity: 1; 26 | animation: opacity 0.5s ease-in-out; 27 | 28 | &.scrollbars-autohide { 29 | opacity:0; 30 | } 31 | 32 | &.visible { 33 | opacity: 1; 34 | } 35 | 36 | 37 | } 38 | .scrollbar-x , .scrollbar-y { 39 | pointer-events:all; 40 | position:absolute; 41 | z-index:100; 42 | 43 | &:hover { 44 | background-color:theme('colors.neutral-200'); 45 | } 46 | 47 | div { 48 | pointer-events:all; 49 | position:absolute; 50 | border-radius:4px; 51 | width:14px; 52 | height:14px; 53 | opacity:0.6; 54 | border:3px solid transparent; 55 | background-color:theme('colors.neutral-500'); 56 | &:hover { 57 | background-color:theme('colors.neutral-900'); 58 | } 59 | 60 | } 61 | 62 | } 63 | 64 | .scrollbar-x { 65 | bottom:0px; 66 | left:0px; 67 | right:14px; 68 | height: 14px; 69 | 70 | 71 | 72 | } 73 | .scrollbar-y { 74 | bottom:14px; 75 | right:0px; 76 | top:0px; 77 | width: 14px; 78 | 79 | 80 | } 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | /*------------------------- 93 | LetMeScroll.js 94 | Made by: Bruno Vieira 95 | --------------------------- */ 96 | 97 | :root { 98 | 99 | /* Colors */ 100 | --lms_scrollbar_bg: #868686; 101 | --lms_scrollbar_radius: 5px; 102 | --lms_scrollpath_bg: transparent; 103 | --lms_scrollpath_radius: 5px; 104 | } 105 | 106 | .lms_scrollable { 107 | margin: 0 auto; 108 | position: relative; 109 | overflow: hidden; 110 | padding-right: 15px; 111 | max-width: 99%; /* 1% is for the scrollbar div */ 112 | } 113 | 114 | .lms_scroll_track 115 | { 116 | background: var(--lms_scrollpath_bg); 117 | border-radius: var(--lms_scrollpath_radius); 118 | width: 8px; 119 | position: absolute; 120 | right: 0px; 121 | height: 100%; 122 | top: 0px; 123 | transition: opacity 0.5s ease; 124 | user-select: none; 125 | } 126 | 127 | .lms_scroll_track{ 128 | animation: appear 1s ease forwards; 129 | } 130 | @keyframes appear { from{ opacity: 0; } to{ opacity: 1; } } 131 | 132 | .lms_scrollable .lms_content_wrapper { 133 | width: 100%; 134 | height: 100%; 135 | overflow-y: scroll; 136 | -ms-overflow-style: none; /* IE and Edge */ 137 | scrollbar-width: none; /* Firefox */ 138 | } 139 | 140 | /* Hide scrollbar for Chrome, Safari and Opera */ 141 | .lms_scrollable .lms_content_wrapper::-webkit-scrollbar { 142 | display: none; 143 | } 144 | 145 | .lms_scroller { 146 | cursor: default; 147 | z-index: 5; 148 | position: absolute; 149 | width: 100%; 150 | border-radius: var(--lms_scrollbar_radius); 151 | background: var(--lms_scrollbar_bg); 152 | top: 0px; 153 | -webkit-transition: top .08s; 154 | -moz-transition: top .08s; 155 | -ms-transition: top .08s; 156 | -o-transition: top .08s; 157 | transition: top .08s; 158 | user-select: none; 159 | transition: opacity 0.5s ease; 160 | } 161 | 162 | -------------------------------------------------------------------------------- /ui/src/scss/debug.scss: -------------------------------------------------------------------------------- 1 | .debug-container { 2 | .label { 3 | display: inline; 4 | } 5 | } -------------------------------------------------------------------------------- /ui/src/scss/details.scss: -------------------------------------------------------------------------------- 1 | 2 | .details-page { 3 | 4 | 5 | } -------------------------------------------------------------------------------- /ui/src/scss/fonts/400-italic.css: -------------------------------------------------------------------------------- 1 | /* roboto-cyrillic-ext-400-italic */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: italic; 5 | font-display: swap; 6 | font-weight: 400; 7 | src: url(./files/roboto-cyrillic-ext-400-italic.woff2) format('woff2'), url(./files/roboto-cyrillic-ext-400-italic.woff) format('woff'); 8 | unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; 9 | } 10 | 11 | /* roboto-cyrillic-400-italic */ 12 | @font-face { 13 | font-family: 'Roboto'; 14 | font-style: italic; 15 | font-display: swap; 16 | font-weight: 400; 17 | src: url(./files/roboto-cyrillic-400-italic.woff2) format('woff2'), url(./files/roboto-cyrillic-400-italic.woff) format('woff'); 18 | unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; 19 | } 20 | 21 | /* roboto-greek-ext-400-italic */ 22 | @font-face { 23 | font-family: 'Roboto'; 24 | font-style: italic; 25 | font-display: swap; 26 | font-weight: 400; 27 | src: url(./files/roboto-greek-ext-400-italic.woff2) format('woff2'), url(./files/roboto-greek-ext-400-italic.woff) format('woff'); 28 | unicode-range: U+1F00-1FFF; 29 | } 30 | 31 | /* roboto-greek-400-italic */ 32 | @font-face { 33 | font-family: 'Roboto'; 34 | font-style: italic; 35 | font-display: swap; 36 | font-weight: 400; 37 | src: url(./files/roboto-greek-400-italic.woff2) format('woff2'), url(./files/roboto-greek-400-italic.woff) format('woff'); 38 | unicode-range: U+0370-03FF; 39 | } 40 | 41 | /* roboto-vietnamese-400-italic */ 42 | @font-face { 43 | font-family: 'Roboto'; 44 | font-style: italic; 45 | font-display: swap; 46 | font-weight: 400; 47 | src: url(./files/roboto-vietnamese-400-italic.woff2) format('woff2'), url(./files/roboto-vietnamese-400-italic.woff) format('woff'); 48 | unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; 49 | } 50 | 51 | /* roboto-latin-ext-400-italic */ 52 | @font-face { 53 | font-family: 'Roboto'; 54 | font-style: italic; 55 | font-display: swap; 56 | font-weight: 400; 57 | src: url(./files/roboto-latin-ext-400-italic.woff2) format('woff2'), url(./files/roboto-latin-ext-400-italic.woff) format('woff'); 58 | unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; 59 | } 60 | 61 | /* roboto-latin-400-italic */ 62 | @font-face { 63 | font-family: 'Roboto'; 64 | font-style: italic; 65 | font-display: swap; 66 | font-weight: 400; 67 | src: url(./files/roboto-latin-400-italic.woff2) format('woff2'), url(./files/roboto-latin-400-italic.woff) format('woff'); 68 | unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; 69 | } -------------------------------------------------------------------------------- /ui/src/scss/fonts/400.css: -------------------------------------------------------------------------------- 1 | /* roboto-cyrillic-ext-400-normal */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: normal; 5 | font-display: swap; 6 | font-weight: 400; 7 | src: url(./files/roboto-cyrillic-ext-400-normal.woff2) format('woff2'), url(./files/roboto-cyrillic-ext-400-normal.woff) format('woff'); 8 | unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; 9 | } 10 | 11 | /* roboto-cyrillic-400-normal */ 12 | @font-face { 13 | font-family: 'Roboto'; 14 | font-style: normal; 15 | font-display: swap; 16 | font-weight: 400; 17 | src: url(./files/roboto-cyrillic-400-normal.woff2) format('woff2'), url(./files/roboto-cyrillic-400-normal.woff) format('woff'); 18 | unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; 19 | } 20 | 21 | /* roboto-greek-ext-400-normal */ 22 | @font-face { 23 | font-family: 'Roboto'; 24 | font-style: normal; 25 | font-display: swap; 26 | font-weight: 400; 27 | src: url(./files/roboto-greek-ext-400-normal.woff2) format('woff2'), url(./files/roboto-greek-ext-400-normal.woff) format('woff'); 28 | unicode-range: U+1F00-1FFF; 29 | } 30 | 31 | /* roboto-greek-400-normal */ 32 | @font-face { 33 | font-family: 'Roboto'; 34 | font-style: normal; 35 | font-display: swap; 36 | font-weight: 400; 37 | src: url(./files/roboto-greek-400-normal.woff2) format('woff2'), url(./files/roboto-greek-400-normal.woff) format('woff'); 38 | unicode-range: U+0370-03FF; 39 | } 40 | 41 | /* roboto-vietnamese-400-normal */ 42 | @font-face { 43 | font-family: 'Roboto'; 44 | font-style: normal; 45 | font-display: swap; 46 | font-weight: 400; 47 | src: url(./files/roboto-vietnamese-400-normal.woff2) format('woff2'), url(./files/roboto-vietnamese-400-normal.woff) format('woff'); 48 | unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; 49 | } 50 | 51 | /* roboto-latin-ext-400-normal */ 52 | @font-face { 53 | font-family: 'Roboto'; 54 | font-style: normal; 55 | font-display: swap; 56 | font-weight: 400; 57 | src: url(./files/roboto-latin-ext-400-normal.woff2) format('woff2'), url(./files/roboto-latin-ext-400-normal.woff) format('woff'); 58 | unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; 59 | } 60 | 61 | /* roboto-latin-400-normal */ 62 | @font-face { 63 | font-family: 'Roboto'; 64 | font-style: normal; 65 | font-display: swap; 66 | font-weight: 400; 67 | src: url(./files/roboto-latin-400-normal.woff2) format('woff2'), url(./files/roboto-latin-400-normal.woff) format('woff'); 68 | unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; 69 | } -------------------------------------------------------------------------------- /ui/src/scss/fonts/500-italic.css: -------------------------------------------------------------------------------- 1 | /* roboto-cyrillic-ext-500-italic */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: italic; 5 | font-display: swap; 6 | font-weight: 500; 7 | src: url(./files/roboto-cyrillic-ext-500-italic.woff2) format('woff2'), url(./files/roboto-cyrillic-ext-500-italic.woff) format('woff'); 8 | unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; 9 | } 10 | 11 | /* roboto-cyrillic-500-italic */ 12 | @font-face { 13 | font-family: 'Roboto'; 14 | font-style: italic; 15 | font-display: swap; 16 | font-weight: 500; 17 | src: url(./files/roboto-cyrillic-500-italic.woff2) format('woff2'), url(./files/roboto-cyrillic-500-italic.woff) format('woff'); 18 | unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; 19 | } 20 | 21 | /* roboto-greek-ext-500-italic */ 22 | @font-face { 23 | font-family: 'Roboto'; 24 | font-style: italic; 25 | font-display: swap; 26 | font-weight: 500; 27 | src: url(./files/roboto-greek-ext-500-italic.woff2) format('woff2'), url(./files/roboto-greek-ext-500-italic.woff) format('woff'); 28 | unicode-range: U+1F00-1FFF; 29 | } 30 | 31 | /* roboto-greek-500-italic */ 32 | @font-face { 33 | font-family: 'Roboto'; 34 | font-style: italic; 35 | font-display: swap; 36 | font-weight: 500; 37 | src: url(./files/roboto-greek-500-italic.woff2) format('woff2'), url(./files/roboto-greek-500-italic.woff) format('woff'); 38 | unicode-range: U+0370-03FF; 39 | } 40 | 41 | /* roboto-vietnamese-500-italic */ 42 | @font-face { 43 | font-family: 'Roboto'; 44 | font-style: italic; 45 | font-display: swap; 46 | font-weight: 500; 47 | src: url(./files/roboto-vietnamese-500-italic.woff2) format('woff2'), url(./files/roboto-vietnamese-500-italic.woff) format('woff'); 48 | unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; 49 | } 50 | 51 | /* roboto-latin-ext-500-italic */ 52 | @font-face { 53 | font-family: 'Roboto'; 54 | font-style: italic; 55 | font-display: swap; 56 | font-weight: 500; 57 | src: url(./files/roboto-latin-ext-500-italic.woff2) format('woff2'), url(./files/roboto-latin-ext-500-italic.woff) format('woff'); 58 | unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; 59 | } 60 | 61 | /* roboto-latin-500-italic */ 62 | @font-face { 63 | font-family: 'Roboto'; 64 | font-style: italic; 65 | font-display: swap; 66 | font-weight: 500; 67 | src: url(./files/roboto-latin-500-italic.woff2) format('woff2'), url(./files/roboto-latin-500-italic.woff) format('woff'); 68 | unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; 69 | } -------------------------------------------------------------------------------- /ui/src/scss/fonts/500.css: -------------------------------------------------------------------------------- 1 | /* roboto-cyrillic-ext-500-normal */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: normal; 5 | font-display: swap; 6 | font-weight: 500; 7 | src: url(./files/roboto-cyrillic-ext-500-normal.woff2) format('woff2'), url(./files/roboto-cyrillic-ext-500-normal.woff) format('woff'); 8 | unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; 9 | } 10 | 11 | /* roboto-cyrillic-500-normal */ 12 | @font-face { 13 | font-family: 'Roboto'; 14 | font-style: normal; 15 | font-display: swap; 16 | font-weight: 500; 17 | src: url(./files/roboto-cyrillic-500-normal.woff2) format('woff2'), url(./files/roboto-cyrillic-500-normal.woff) format('woff'); 18 | unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; 19 | } 20 | 21 | /* roboto-greek-ext-500-normal */ 22 | @font-face { 23 | font-family: 'Roboto'; 24 | font-style: normal; 25 | font-display: swap; 26 | font-weight: 500; 27 | src: url(./files/roboto-greek-ext-500-normal.woff2) format('woff2'), url(./files/roboto-greek-ext-500-normal.woff) format('woff'); 28 | unicode-range: U+1F00-1FFF; 29 | } 30 | 31 | /* roboto-greek-500-normal */ 32 | @font-face { 33 | font-family: 'Roboto'; 34 | font-style: normal; 35 | font-display: swap; 36 | font-weight: 500; 37 | src: url(./files/roboto-greek-500-normal.woff2) format('woff2'), url(./files/roboto-greek-500-normal.woff) format('woff'); 38 | unicode-range: U+0370-03FF; 39 | } 40 | 41 | /* roboto-vietnamese-500-normal */ 42 | @font-face { 43 | font-family: 'Roboto'; 44 | font-style: normal; 45 | font-display: swap; 46 | font-weight: 500; 47 | src: url(./files/roboto-vietnamese-500-normal.woff2) format('woff2'), url(./files/roboto-vietnamese-500-normal.woff) format('woff'); 48 | unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; 49 | } 50 | 51 | /* roboto-latin-ext-500-normal */ 52 | @font-face { 53 | font-family: 'Roboto'; 54 | font-style: normal; 55 | font-display: swap; 56 | font-weight: 500; 57 | src: url(./files/roboto-latin-ext-500-normal.woff2) format('woff2'), url(./files/roboto-latin-ext-500-normal.woff) format('woff'); 58 | unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; 59 | } 60 | 61 | /* roboto-latin-500-normal */ 62 | @font-face { 63 | font-family: 'Roboto'; 64 | font-style: normal; 65 | font-display: swap; 66 | font-weight: 500; 67 | src: url(./files/roboto-latin-500-normal.woff2) format('woff2'), url(./files/roboto-latin-500-normal.woff) format('woff'); 68 | unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; 69 | } -------------------------------------------------------------------------------- /ui/src/scss/fonts/700-italic.css: -------------------------------------------------------------------------------- 1 | /* roboto-cyrillic-ext-700-italic */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: italic; 5 | font-display: swap; 6 | font-weight: 700; 7 | src: url(./files/roboto-cyrillic-ext-700-italic.woff2) format('woff2'), url(./files/roboto-cyrillic-ext-700-italic.woff) format('woff'); 8 | unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; 9 | } 10 | 11 | /* roboto-cyrillic-700-italic */ 12 | @font-face { 13 | font-family: 'Roboto'; 14 | font-style: italic; 15 | font-display: swap; 16 | font-weight: 700; 17 | src: url(./files/roboto-cyrillic-700-italic.woff2) format('woff2'), url(./files/roboto-cyrillic-700-italic.woff) format('woff'); 18 | unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; 19 | } 20 | 21 | /* roboto-greek-ext-700-italic */ 22 | @font-face { 23 | font-family: 'Roboto'; 24 | font-style: italic; 25 | font-display: swap; 26 | font-weight: 700; 27 | src: url(./files/roboto-greek-ext-700-italic.woff2) format('woff2'), url(./files/roboto-greek-ext-700-italic.woff) format('woff'); 28 | unicode-range: U+1F00-1FFF; 29 | } 30 | 31 | /* roboto-greek-700-italic */ 32 | @font-face { 33 | font-family: 'Roboto'; 34 | font-style: italic; 35 | font-display: swap; 36 | font-weight: 700; 37 | src: url(./files/roboto-greek-700-italic.woff2) format('woff2'), url(./files/roboto-greek-700-italic.woff) format('woff'); 38 | unicode-range: U+0370-03FF; 39 | } 40 | 41 | /* roboto-vietnamese-700-italic */ 42 | @font-face { 43 | font-family: 'Roboto'; 44 | font-style: italic; 45 | font-display: swap; 46 | font-weight: 700; 47 | src: url(./files/roboto-vietnamese-700-italic.woff2) format('woff2'), url(./files/roboto-vietnamese-700-italic.woff) format('woff'); 48 | unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; 49 | } 50 | 51 | /* roboto-latin-ext-700-italic */ 52 | @font-face { 53 | font-family: 'Roboto'; 54 | font-style: italic; 55 | font-display: swap; 56 | font-weight: 700; 57 | src: url(./files/roboto-latin-ext-700-italic.woff2) format('woff2'), url(./files/roboto-latin-ext-700-italic.woff) format('woff'); 58 | unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; 59 | } 60 | 61 | /* roboto-latin-700-italic */ 62 | @font-face { 63 | font-family: 'Roboto'; 64 | font-style: italic; 65 | font-display: swap; 66 | font-weight: 700; 67 | src: url(./files/roboto-latin-700-italic.woff2) format('woff2'), url(./files/roboto-latin-700-italic.woff) format('woff'); 68 | unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; 69 | } -------------------------------------------------------------------------------- /ui/src/scss/fonts/700.css: -------------------------------------------------------------------------------- 1 | /* roboto-cyrillic-ext-700-normal */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: normal; 5 | font-display: swap; 6 | font-weight: 700; 7 | src: url(./files/roboto-cyrillic-ext-700-normal.woff2) format('woff2'), url(./files/roboto-cyrillic-ext-700-normal.woff) format('woff'); 8 | unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; 9 | } 10 | 11 | /* roboto-cyrillic-700-normal */ 12 | @font-face { 13 | font-family: 'Roboto'; 14 | font-style: normal; 15 | font-display: swap; 16 | font-weight: 700; 17 | src: url(./files/roboto-cyrillic-700-normal.woff2) format('woff2'), url(./files/roboto-cyrillic-700-normal.woff) format('woff'); 18 | unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; 19 | } 20 | 21 | /* roboto-greek-ext-700-normal */ 22 | @font-face { 23 | font-family: 'Roboto'; 24 | font-style: normal; 25 | font-display: swap; 26 | font-weight: 700; 27 | src: url(./files/roboto-greek-ext-700-normal.woff2) format('woff2'), url(./files/roboto-greek-ext-700-normal.woff) format('woff'); 28 | unicode-range: U+1F00-1FFF; 29 | } 30 | 31 | /* roboto-greek-700-normal */ 32 | @font-face { 33 | font-family: 'Roboto'; 34 | font-style: normal; 35 | font-display: swap; 36 | font-weight: 700; 37 | src: url(./files/roboto-greek-700-normal.woff2) format('woff2'), url(./files/roboto-greek-700-normal.woff) format('woff'); 38 | unicode-range: U+0370-03FF; 39 | } 40 | 41 | /* roboto-vietnamese-700-normal */ 42 | @font-face { 43 | font-family: 'Roboto'; 44 | font-style: normal; 45 | font-display: swap; 46 | font-weight: 700; 47 | src: url(./files/roboto-vietnamese-700-normal.woff2) format('woff2'), url(./files/roboto-vietnamese-700-normal.woff) format('woff'); 48 | unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; 49 | } 50 | 51 | /* roboto-latin-ext-700-normal */ 52 | @font-face { 53 | font-family: 'Roboto'; 54 | font-style: normal; 55 | font-display: swap; 56 | font-weight: 700; 57 | src: url(./files/roboto-latin-ext-700-normal.woff2) format('woff2'), url(./files/roboto-latin-ext-700-normal.woff) format('woff'); 58 | unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; 59 | } 60 | 61 | /* roboto-latin-700-normal */ 62 | @font-face { 63 | font-family: 'Roboto'; 64 | font-style: normal; 65 | font-display: swap; 66 | font-weight: 700; 67 | src: url(./files/roboto-latin-700-normal.woff2) format('woff2'), url(./files/roboto-latin-700-normal.woff) format('woff'); 68 | unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; 69 | } -------------------------------------------------------------------------------- /ui/src/scss/fonts/mono.css: -------------------------------------------------------------------------------- 1 | /* roboto-mono-cyrillic-ext-wght-normal */ 2 | @font-face { 3 | font-family: 'Roboto Mono Variable'; 4 | font-style: normal; 5 | font-display: swap; 6 | font-weight: 100 700; 7 | src: url(./files/roboto-mono-cyrillic-ext-wght-normal.woff2) format('woff2-variations'); 8 | unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; 9 | } 10 | 11 | /* roboto-mono-cyrillic-wght-normal */ 12 | @font-face { 13 | font-family: 'Roboto Mono Variable'; 14 | font-style: normal; 15 | font-display: swap; 16 | font-weight: 100 700; 17 | src: url(./files/roboto-mono-cyrillic-wght-normal.woff2) format('woff2-variations'); 18 | unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; 19 | } 20 | 21 | /* roboto-mono-greek-wght-normal */ 22 | @font-face { 23 | font-family: 'Roboto Mono Variable'; 24 | font-style: normal; 25 | font-display: swap; 26 | font-weight: 100 700; 27 | src: url(./files/roboto-mono-greek-wght-normal.woff2) format('woff2-variations'); 28 | unicode-range: U+0370-03FF; 29 | } 30 | 31 | /* roboto-mono-vietnamese-wght-normal */ 32 | @font-face { 33 | font-family: 'Roboto Mono Variable'; 34 | font-style: normal; 35 | font-display: swap; 36 | font-weight: 100 700; 37 | src: url(./files/roboto-mono-vietnamese-wght-normal.woff2) format('woff2-variations'); 38 | unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; 39 | } 40 | 41 | /* roboto-mono-latin-ext-wght-normal */ 42 | @font-face { 43 | font-family: 'Roboto Mono Variable'; 44 | font-style: normal; 45 | font-display: swap; 46 | font-weight: 100 700; 47 | src: url(./files/roboto-mono-latin-ext-wght-normal.woff2) format('woff2-variations'); 48 | unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; 49 | } 50 | 51 | /* roboto-mono-latin-wght-normal */ 52 | @font-face { 53 | font-family: 'Roboto Mono Variable'; 54 | font-style: normal; 55 | font-display: swap; 56 | font-weight: 100 700; 57 | src: url(./files/roboto-mono-latin-wght-normal.woff2) format('woff2-variations'); 58 | unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; 59 | } -------------------------------------------------------------------------------- /ui/src/scss/layout.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | overflow:hidden; 3 | } 4 | 5 | 6 | 7 | .browser-layout{ 8 | display:flex; 9 | flex-direction:row; 10 | align-items: stretch; 11 | overflow:hidden; 12 | height:100vh; 13 | width:100vw; 14 | max-width: 100vw; 15 | max-height: 100vh; 16 | 17 | .browser-work-area { 18 | flex-grow:1; 19 | flex-shrink:1; 20 | overflow:hidden; 21 | } 22 | 23 | .browser-nav { 24 | flex-basis: 6.5rem; 25 | max-width: 6.5rem; 26 | flex-grow: 0; 27 | flex-shrink: 0; 28 | display:flex; 29 | flex-direction: column; 30 | 31 | ul { 32 | flex-grow:2; 33 | flex-shrink: 0; 34 | 35 | :where(li:empty) { 36 | --tw-bg-opacity: 1; 37 | background-color: none; 38 | opacity: 0.1; 39 | margin: 0.5rem 1rem; 40 | height: 0px; 41 | } 42 | 43 | li { a { 44 | 45 | display: block; 46 | max-height:5.5rem; 47 | height:5.5rem; 48 | max-width:5.5rem; 49 | padding:0.5rem; 50 | margin-bottom: 0.5rem; 51 | 52 | svg {width:2.5rem; height:2.5rem; display: block; margin:0.2rem auto 0.3rem auto} 53 | span {font-size: 0.8rem; display: block; margin:auto; text-align: center;} 54 | 55 | } 56 | label { 57 | max-height:5.5rem; 58 | 59 | max-width:5.5rem; 60 | padding:0.5rem; 61 | margin-bottom: 0.5rem; 62 | } 63 | .disconnected-status, .login-status { 64 | svg {width:2rem; height:2rem; display: block; margin:0.2rem auto 0.3rem auto} 65 | max-height:3.5rem; 66 | 67 | max-width:5.5rem; 68 | padding:0.5rem; 69 | margin-bottom: 0.5rem; 70 | display: block; 71 | 72 | } 73 | 74 | } 75 | } 76 | 77 | ul.global-take { 78 | 79 | flex-grow:0; 80 | flex-shrink: 0; 81 | padding-top:1rem; 82 | 83 | li a { 84 | max-height:3.7rem; 85 | height:3.7rem; 86 | max-width:4rem; 87 | padding:0.2rem; 88 | margin: 0.2rem 1rem; 89 | 90 | span {font-size: 1.2rem; margin-top:1.15rem; font-weight: 700;} 91 | } 92 | 93 | li label { 94 | display:block; 95 | text-align: center; 96 | span { 97 | display:block; 98 | text-align: center; 99 | margin-bottom:0.3rem; 100 | } 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /ui/src/scss/setup.scss: -------------------------------------------------------------------------------- 1 | .setup-container { 2 | .label { 3 | display: inline; 4 | } 5 | } -------------------------------------------------------------------------------- /ui/src/scss/topology.scss: -------------------------------------------------------------------------------- 1 | $tp-border-color : #222222; 2 | $tp-device-color : #999999; 3 | $tp-int-color : #444444; 4 | 5 | $tp-round:6px; 6 | $tp-padding:2px; 7 | $tp-int-size:30px; 8 | $tp-int-height:20px; 9 | $tp-int-qsize:60px; 10 | $tp-int-splitsize:14px; 11 | 12 | .topology-container { 13 | 14 | 15 | 16 | .tp-switch-node { 17 | display:flex; 18 | flex-direction:column; 19 | border:1px solid $tp-border-color; 20 | background-color:$tp-device-color; 21 | border-radius: $tp-round; 22 | padding: 0px $tp-round; 23 | color:#111111; 24 | 25 | .tp-switch-description { 26 | padding: $tp-padding $tp-round; 27 | font-size: 14px; 28 | } 29 | 30 | .tp-switch-row-top, .tp-switch-row-bottom { 31 | padding:0px; 32 | display:flex; 33 | flex-direction: row; 34 | 35 | .tp-switch-interface { 36 | position:relative; 37 | text-align: center; 38 | color:#eeeeee; 39 | margin:0px $tp-padding; 40 | overflow:hidden; 41 | font-size: 10px; 42 | width:$tp-int-size; 43 | height:$tp-int-height; 44 | background-color:$tp-int-color; 45 | .svelte-flow__handle { 46 | opacity: 0; 47 | left:0px; 48 | top:0px; 49 | bottom:0px; 50 | right:0px; 51 | width:unset; 52 | height:unset; 53 | border-radius: 0px; 54 | transform:unset; 55 | } 56 | } 57 | 58 | .tp-switch-interface-qsfp { 59 | width:$tp-int-qsize; 60 | } 61 | 62 | .tp-switch-interface-qsfp-split { 63 | 64 | width:$tp-int-splitsize; 65 | margin-left:0px; 66 | margin-right:0px; 67 | &:nth-of-type(4n+1) { 68 | margin-left:2px; 69 | } 70 | &:nth-of-type(4n) { 71 | margin-right:2px; 72 | } 73 | } 74 | } 75 | 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /ui/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /ui/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/routes/**/*.{svelte,js,ts}', "./safelist.txt"], 3 | plugins: [require('daisyui')], 4 | 5 | daisyui: { 6 | themes: false, // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"] 7 | 8 | darkTheme: "dark", // name of one of the included themes for dark mode 9 | base: true, // applies background color and foreground color for root element by default 10 | styled: true, // include daisyUI colors and design decisions for all components 11 | utils: true, // adds responsive and modifier utility classes 12 | prefix: "", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors) 13 | logs: true, // Shows info about daisyUI version and used config in the console when building your CSS 14 | themeRoot: ":root", // The element that receives theme color CSS variables 15 | }, 16 | 17 | theme: { 18 | extend: { 19 | fontFamily: { 20 | sans: ['Roboto'], 21 | mono: ['"Roboto Mono Variable"'], 22 | }, 23 | }, 24 | }, 25 | }; -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true 17 | }, 18 | "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | 4 | 5 | function setHeaders(request, response, next) { 6 | if(request.originalUrl == "/assets/connectionWorker.js"){ 7 | response.setHeader("Service-Worker-Allowed", "/"); 8 | response.setHeader("Cahce-Control", "no-cache"); 9 | } 10 | next(); 11 | } 12 | 13 | const customHeaders = { 14 | name: 'customHeaders', 15 | configureServer: server => { server.middlewares.use(setHeaders); }, 16 | configurePreviewServer: server => { server.middlewares.use(setHeaders); }, 17 | }; 18 | 19 | 20 | // https://vitejs.dev/config/ 21 | export default defineConfig({ 22 | plugins: [svelte()], 23 | build: { 24 | outDir: '../server/public', 25 | emptyOutDir: true, // also necessary 26 | }, 27 | 28 | server: { 29 | proxy: { 30 | 31 | // Proxying websockets or socket.io: ws://localhost:5173/socket.io -> ws://localhost:5174/socket.io 32 | '/sync': { 33 | target: 'ws://localhost:80', 34 | ws: true, 35 | }, 36 | }, 37 | }, 38 | }) 39 | --------------------------------------------------------------------------------