Merge "Update undistort to iterate until convergence"
diff --git a/.bazelignore b/.bazelignore
index 3665125..0b2e110 100644
--- a/.bazelignore
+++ b/.bazelignore
@@ -10,3 +10,4 @@
scouting/www/shift_schedule/node_modules
scouting/www/view/node_modules
scouting/www/pit_scouting/node_modules
+scouting/www/scan/node_modules
diff --git a/WORKSPACE b/WORKSPACE
index 82a0dd4..1aabe54 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -919,6 +919,7 @@
"@//scouting/www/match_list:package.json",
"@//scouting/www/notes:package.json",
"@//scouting/www/rpc:package.json",
+ "@//scouting/www/scan:package.json",
"@//scouting/www/shift_schedule:package.json",
"@//scouting/www/view:package.json",
],
@@ -988,6 +989,11 @@
http_archive(
name = "aspect_rules_cypress",
+ patch_args = ["-p1"],
+ patches = [
+ "//third_party:rules_cypress/0001-fix-incorrect-linux-checksums.patch",
+ "//third_party:rules_cypress/0002-Add-support-for-cypress-13.6.6.patch",
+ ],
sha256 = "76947778d8e855eee3c15931e1fcdc1c2a25d56d6c0edd110b2227c05b794d08",
strip_prefix = "rules_cypress-0.3.2",
urls = [
@@ -1002,7 +1008,7 @@
cypress_register_toolchains(
name = "cypress",
- cypress_version = "12.3.0",
+ cypress_version = "13.3.1",
)
# Copied from:
@@ -1013,7 +1019,7 @@
# LASTCHANGE_URL="https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2FLAST_CHANGE?alt=media"
# CHROME_REVISION=$(curl -s -S $LASTCHANGE_URL)
# echo "latest CHROME_REVISION_LINUX is $CHROME_REVISION"
-CHROME_REVISION_LINUX = "1072361"
+CHROME_REVISION_LINUX = "1264932"
http_archive(
name = "chrome_linux",
@@ -1022,7 +1028,7 @@
srcs = glob(["**"]),
visibility = ["//visibility:public"],
)""",
- sha256 = "0df22f743facd1e090eff9b7f8d8bdc293fb4dc31ce9156d2ef19b515974a72b",
+ sha256 = "4de54f43b2fc4812b9fad4145e44df6ed3063969174a8883ea42ed4c1ee58301",
strip_prefix = "chrome-linux",
urls = [
"https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F" + CHROME_REVISION_LINUX + "%2Fchrome-linux.zip?alt=media",
@@ -1207,6 +1213,14 @@
url = "https://software.frc971.org/Build-Dependencies/2021-10-03_superstructure_shoot_balls.tar.gz",
)
+http_file(
+ name = "opencv_wasm",
+ sha256 = "447244d0e67e411f91e7c225c07f104437104e3e753085248a0c527a25bd8807",
+ urls = [
+ "https://docs.opencv.org/4.9.0/opencv.js",
+ ],
+)
+
http_archive(
name = "opencv_k8",
build_file = "@//debian:opencv.BUILD",
diff --git a/frc971/vision/target_mapper.cc b/frc971/vision/target_mapper.cc
index c14b032..f5c8dab 100644
--- a/frc971/vision/target_mapper.cc
+++ b/frc971/vision/target_mapper.cc
@@ -23,6 +23,8 @@
DEFINE_double(outlier_std_devs, 1.0,
"Number of standard deviations above average error needed for a "
"constraint to be considered an outlier and get removed.");
+DEFINE_bool(do_map_fitting, false,
+ "Whether to do a final fit of the solved map to the original map");
namespace frc971::vision {
Eigen::Affine3d PoseUtils::Pose3dToAffine3d(
@@ -448,36 +450,38 @@
CHECK(SolveOptimizationProblem(&target_pose_problem_2))
<< "The target pose solve 2 was not successful, exiting.";
- LOG(INFO) << "Solving the overall map's best alignment to the previous map";
- ceres::Problem map_fitting_problem(
- {.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP});
- std::unique_ptr<ceres::CostFunction> map_fitting_cost_function =
- BuildMapFittingOptimizationProblem(&map_fitting_problem);
- CHECK(SolveOptimizationProblem(&map_fitting_problem))
- << "The map fitting solve was not successful, exiting.";
- map_fitting_cost_function.release();
+ if (FLAGS_do_map_fitting) {
+ LOG(INFO) << "Solving the overall map's best alignment to the previous map";
+ ceres::Problem map_fitting_problem(
+ {.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP});
+ std::unique_ptr<ceres::CostFunction> map_fitting_cost_function =
+ BuildMapFittingOptimizationProblem(&map_fitting_problem);
+ CHECK(SolveOptimizationProblem(&map_fitting_problem))
+ << "The map fitting solve was not successful, exiting.";
+ map_fitting_cost_function.release();
- Eigen::Affine3d H_frozen_actual = T_frozen_actual_ * R_frozen_actual_;
- LOG(INFO) << "H_frozen_actual: "
- << PoseUtils::Affine3dToPose3d(H_frozen_actual);
+ Eigen::Affine3d H_frozen_actual = T_frozen_actual_ * R_frozen_actual_;
+ LOG(INFO) << "H_frozen_actual: "
+ << PoseUtils::Affine3dToPose3d(H_frozen_actual);
- auto H_world_frozen =
- PoseUtils::Pose3dToAffine3d(target_poses_[FLAGS_frozen_target_id]);
- auto H_world_frozenactual = H_world_frozen * H_frozen_actual;
+ auto H_world_frozen =
+ PoseUtils::Pose3dToAffine3d(target_poses_[FLAGS_frozen_target_id]);
+ auto H_world_frozenactual = H_world_frozen * H_frozen_actual;
- // Offset the solved poses to become the actual ones
- for (auto &[id, pose] : target_poses_) {
- // Don't offset targets we didn't solve for
- if (id < FLAGS_min_target_id || id > FLAGS_max_target_id) {
- continue;
+ // Offset the solved poses to become the actual ones
+ for (auto &[id, pose] : target_poses_) {
+ // Don't offset targets we didn't solve for
+ if (id < FLAGS_min_target_id || id > FLAGS_max_target_id) {
+ continue;
+ }
+
+ // Take the delta between the frozen target and the solved target, and put
+ // that on top of the actual pose of the frozen target
+ auto H_world_solved = PoseUtils::Pose3dToAffine3d(pose);
+ auto H_frozen_solved = H_world_frozen.inverse() * H_world_solved;
+ auto H_world_actual = H_world_frozenactual * H_frozen_solved;
+ pose = PoseUtils::Affine3dToPose3d(H_world_actual);
}
-
- // Take the delta between the frozen target and the solved target, and put
- // that on top of the actual pose of the frozen target
- auto H_world_solved = PoseUtils::Pose3dToAffine3d(pose);
- auto H_frozen_solved = H_world_frozen.inverse() * H_world_solved;
- auto H_world_actual = H_world_frozenactual * H_frozen_solved;
- pose = PoseUtils::Affine3dToPose3d(H_world_actual);
}
auto map_json = MapToJson(field_name);
diff --git a/package.json b/package.json
index df5df67..6853a14 100644
--- a/package.json
+++ b/package.json
@@ -11,18 +11,23 @@
"@angular/core": "v16-lts",
"@angular/forms": "v16-lts",
"@angular/platform-browser": "v16-lts",
+ "@angular/service-worker": "v16-lts",
"@angular/cli": "v16-lts",
"@babel/cli": "^7.16.0",
"@babel/core": "^7.16.0",
"@types/jasmine": "3.10.3",
"@types/babel__core": "^7.20.5",
"@types/babel__generator": "^7.6.8",
+ "@types/pako": "2.0.3",
+ "angularx-qrcode": "^16.0.2",
"html-insert-assets": "0.14.3",
- "cypress": "12.3.0",
+ "cypress": "13.3.1",
+ "pako": "2.1.0",
"prettier": "2.6.1",
"requirejs": "2.3.6",
"rollup": "4.12.0",
"rxjs": "7.5.7",
+ "dexie": "^3.2.5",
"@rollup/plugin-node-resolve": "15.2.3",
"@types/flatbuffers": "1.10.0",
"@types/node": "20.11.19",
@@ -30,4 +35,4 @@
"terser": "5.16.4",
"zone.js": "^0.13.0"
}
-}
+}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8f37b3b..bf6a429 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,6 +32,9 @@
'@angular/platform-browser':
specifier: v16-lts
version: 16.2.12(@angular/animations@16.2.12)(@angular/common@16.2.12)(@angular/core@16.2.12)
+ '@angular/service-worker':
+ specifier: v16-lts
+ version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)
'@babel/cli':
specifier: ^7.16.0
version: 7.23.9(@babel/core@7.23.9)
@@ -56,12 +59,24 @@
'@types/node':
specifier: 20.11.19
version: 20.11.19
+ '@types/pako':
+ specifier: 2.0.3
+ version: 2.0.3
+ angularx-qrcode:
+ specifier: ^16.0.2
+ version: 16.0.2(@angular/core@16.2.12)
cypress:
- specifier: 12.3.0
- version: 12.3.0
+ specifier: 13.3.1
+ version: 13.3.1
+ dexie:
+ specifier: ^3.2.5
+ version: 3.2.5(karma@6.4.3)
html-insert-assets:
specifier: 0.14.3
version: 0.14.3
+ pako:
+ specifier: 2.1.0
+ version: 2.1.0
prettier:
specifier: 2.6.1
version: 2.6.1
@@ -102,6 +117,12 @@
'@org_frc971/scouting/www/counter_button':
specifier: workspace:*
version: link:../counter_button
+ '@types/pako':
+ specifier: 2.0.3
+ version: 2.0.3
+ pako:
+ specifier: 2.1.0
+ version: 2.1.0
scouting/www/match_list:
dependencies:
@@ -126,6 +147,18 @@
scouting/www/rpc: {}
+ scouting/www/scan:
+ dependencies:
+ '@angular/forms':
+ specifier: v16-lts
+ version: 16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12)(@angular/platform-browser@16.2.12)(rxjs@7.5.7)
+ '@types/pako':
+ specifier: 2.0.3
+ version: 2.0.3
+ pako:
+ specifier: 2.1.0
+ version: 2.1.0
+
scouting/www/shift_schedule:
dependencies:
'@angular/forms':
@@ -314,6 +347,19 @@
'@angular/core': 16.2.12(rxjs@7.5.7)(zone.js@0.13.3)
tslib: 2.6.0
+ /@angular/service-worker@16.2.12(@angular/common@16.2.12)(@angular/core@16.2.12):
+ resolution: {integrity: sha512-o0z0s4c76NmRASa+mUHn/q6vUKQNa06mGmLBDKm84vRQ1sQ2TJv+R1p8K9WkiM5mGy6tjQCDOgaz13TcxMFWOQ==}
+ engines: {node: ^16.14.0 || >=18.10.0}
+ hasBin: true
+ peerDependencies:
+ '@angular/common': 16.2.12
+ '@angular/core': 16.2.12
+ dependencies:
+ '@angular/common': 16.2.12(@angular/core@16.2.12)(rxjs@7.5.7)
+ '@angular/core': 16.2.12(rxjs@7.5.7)(zone.js@0.13.3)
+ tslib: 2.6.0
+ dev: true
+
/@babel/cli@7.23.9(@babel/core@7.23.9):
resolution: {integrity: sha512-vB1UXmGDNEhcf1jNAHKT9IlYk1R+hehVTLFlCLHBi8gfuHQGP6uRjgXVYU0EVlI/qwAWpstqkBdf2aez3/z/5Q==}
engines: {node: '>=6.9.0'}
@@ -567,10 +613,9 @@
engines: {node: '>=0.1.90'}
requiresBuild: true
dev: true
- optional: true
- /@cypress/request@2.88.11:
- resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==}
+ /@cypress/request@3.0.1:
+ resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==}
engines: {node: '>= 6'}
dependencies:
aws-sign2: 0.7.0
@@ -588,7 +633,7 @@
performance-now: 2.1.0
qs: 6.10.4
safe-buffer: 5.2.1
- tough-cookie: 2.5.0
+ tough-cookie: 4.1.3
tunnel-agent: 0.6.0
uuid: 8.3.2
dev: true
@@ -925,6 +970,10 @@
- supports-color
dev: true
+ /@socket.io/component-emitter@3.1.0:
+ resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==}
+ dev: true
+
/@tootallnate/once@2.0.0:
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
@@ -972,6 +1021,16 @@
'@babel/types': 7.23.9
dev: true
+ /@types/cookie@0.4.1:
+ resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
+ dev: true
+
+ /@types/cors@2.8.17:
+ resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
+ dependencies:
+ '@types/node': 20.11.19
+ dev: true
+
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
dev: true
@@ -984,8 +1043,10 @@
resolution: {integrity: sha512-SWyMrjgdAUHNQmutvDcKablrJhkDLy4wunTme8oYLjKp41GnHGxMRXr2MQMvy/qy8H3LdzwQk9gH4hZ6T++H8g==}
dev: true
- /@types/node@14.18.53:
- resolution: {integrity: sha512-soGmOpVBUq+gaBMwom1M+krC/NNbWlosh4AtGA03SyWNDiqSKtwp7OulO1M6+mg8YkHMvJ/y0AkCeO8d1hNb7A==}
+ /@types/node@18.19.22:
+ resolution: {integrity: sha512-p3pDIfuMg/aXBmhkyanPshdfJuX5c5+bQjYLIikPLXAUycEogij/c50n/C+8XOA5L93cU4ZRXtn+dNQGi0IZqQ==}
+ dependencies:
+ undici-types: 5.26.5
dev: true
/@types/node@20.11.19:
@@ -994,6 +1055,9 @@
undici-types: 5.26.5
dev: true
+ /@types/pako@2.0.3:
+ resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==}
+
/@types/resolve@1.20.2:
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
dev: true
@@ -1022,6 +1086,14 @@
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: true
+ /accepts@1.3.8:
+ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-types: 2.1.35
+ negotiator: 0.6.3
+ dev: true
+
/acorn@8.9.0:
resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==}
engines: {node: '>=0.4.0'}
@@ -1072,6 +1144,16 @@
uri-js: 4.4.1
dev: true
+ /angularx-qrcode@16.0.2(@angular/core@16.2.12):
+ resolution: {integrity: sha512-FztOM7vjNu88sGxUU5jG2I+A9TxZBXXYBWINjpwIBbTL+COMgrtzXnScG7TyQeNknv5w3WFJWn59PcngRRYVXA==}
+ peerDependencies:
+ '@angular/core': ^16.0.0
+ dependencies:
+ '@angular/core': 16.2.12(rxjs@7.5.7)(zone.js@0.13.3)
+ qrcode: 1.5.3
+ tslib: 2.6.0
+ dev: true
+
/ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@@ -1182,6 +1264,11 @@
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
+ /base64id@2.0.0:
+ resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
+ engines: {node: ^4.5.0 || >= 5.9}
+ dev: true
+
/bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
dependencies:
@@ -1209,6 +1296,26 @@
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
dev: true
+ /body-parser@1.20.2:
+ resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 2.6.9
+ depd: 2.0.0
+ destroy: 1.2.0
+ http-errors: 2.0.0
+ iconv-lite: 0.4.24
+ on-finished: 2.4.1
+ qs: 6.11.0
+ raw-body: 2.5.2
+ type-is: 1.6.18
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
@@ -1266,6 +1373,11 @@
semver: 7.6.0
dev: true
+ /bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
/cacache@16.1.3:
resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@@ -1318,10 +1430,15 @@
/call-bind@1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
- function-bind: 1.1.1
+ function-bind: 1.1.2
get-intrinsic: 1.2.1
dev: true
+ /camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+ dev: true
+
/caniuse-lite@1.0.30001588:
resolution: {integrity: sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==}
dev: true
@@ -1420,6 +1537,22 @@
engines: {node: '>= 10'}
dev: true
+ /cliui@6.0.0:
+ resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
+ dev: true
+
+ /cliui@7.0.4:
+ resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+ dev: true
+
/cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@@ -1480,8 +1613,8 @@
engines: {node: '>= 6'}
dev: true
- /commander@5.1.0:
- resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
+ /commander@6.2.1:
+ resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
engines: {node: '>= 6'}
dev: true
@@ -1494,10 +1627,27 @@
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
+ /connect@3.7.0:
+ resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
+ engines: {node: '>= 0.10.0'}
+ dependencies:
+ debug: 2.6.9
+ finalhandler: 1.1.2
+ parseurl: 1.3.3
+ utils-merge: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
dev: true
+ /content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
/convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
dev: true
@@ -1506,10 +1656,23 @@
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
dev: true
+ /cookie@0.4.2:
+ resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
/core-util-is@1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
dev: true
+ /cors@2.8.5:
+ resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
+ engines: {node: '>= 0.10'}
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+ dev: true
+
/cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@@ -1519,15 +1682,19 @@
which: 2.0.2
dev: true
- /cypress@12.3.0:
- resolution: {integrity: sha512-ZQNebibi6NBt51TRxRMYKeFvIiQZ01t50HSy7z/JMgRVqBUey3cdjog5MYEbzG6Ktti5ckDt1tfcC47lmFwXkw==}
- engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
+ /custom-event@1.0.1:
+ resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==}
+ dev: true
+
+ /cypress@13.3.1:
+ resolution: {integrity: sha512-g4mJLZxYN+UAF2LMy3Znd4LBnUmS59Vynd81VES59RdW48Yt+QtR2cush3melOoVNz0PPbADpWr8DcUx6mif8Q==}
+ engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
hasBin: true
requiresBuild: true
dependencies:
- '@cypress/request': 2.88.11
+ '@cypress/request': 3.0.1
'@cypress/xvfb': 1.2.4(supports-color@8.1.1)
- '@types/node': 14.18.53
+ '@types/node': 18.19.22
'@types/sinonjs__fake-timers': 8.1.1
'@types/sizzle': 2.3.3
arch: 2.2.0
@@ -1539,7 +1706,7 @@
check-more-types: 2.24.0
cli-cursor: 3.1.0
cli-table3: 0.6.3
- commander: 5.1.0
+ commander: 6.2.1
common-tags: 1.8.2
dayjs: 1.11.9
debug: 4.3.4(supports-color@8.1.1)
@@ -1560,9 +1727,10 @@
minimist: 1.2.8
ospath: 1.2.2
pretty-bytes: 5.6.0
+ process: 0.11.10
proxy-from-env: 1.0.0
request-progress: 3.0.0
- semver: 7.5.3
+ semver: 7.6.0
supports-color: 8.1.1
tmp: 0.2.1
untildify: 4.0.0
@@ -1576,10 +1744,26 @@
assert-plus: 1.0.0
dev: true
+ /date-format@4.0.14:
+ resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
+ engines: {node: '>=4.0'}
+ dev: true
+
/dayjs@1.11.9:
resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
dev: true
+ /debug@2.6.9:
+ resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.0.0
+ dev: true
+
/debug@3.2.7(supports-color@8.1.1):
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -1605,6 +1789,11 @@
supports-color: 8.1.1
dev: true
+ /decamelize@1.2.0:
+ resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
/deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
@@ -1630,6 +1819,42 @@
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
dev: true
+ /depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
+ /destroy@1.2.0:
+ resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+ dev: true
+
+ /dexie@3.2.5(karma@6.4.3):
+ resolution: {integrity: sha512-MA7vYQvXxWN2+G50D0GLS4FqdYUyRYQsN0FikZIVebOmRoNCSCL9+eUbIF80dqrfns3kmY+83+hE2GN9CnAGyA==}
+ engines: {node: '>=6.0'}
+ dependencies:
+ karma-safari-launcher: 1.0.0(karma@6.4.3)
+ transitivePeerDependencies:
+ - karma
+ dev: true
+
+ /di@0.0.1:
+ resolution: {integrity: sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==}
+ dev: true
+
+ /dijkstrajs@1.0.3:
+ resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
+ dev: true
+
+ /dom-serialize@2.2.1:
+ resolution: {integrity: sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==}
+ dependencies:
+ custom-event: 1.0.1
+ ent: 2.2.0
+ extend: 3.0.2
+ void-elements: 2.0.1
+ dev: true
+
/eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
@@ -1641,6 +1866,10 @@
safer-buffer: 2.1.2
dev: true
+ /ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+ dev: true
+
/electron-to-chromium@1.4.679:
resolution: {integrity: sha512-NhQMsz5k0d6m9z3qAxnsOR/ebal4NAGsrNVRwcDo4Kc/zQ7KdsTKZUxZoygHcVRb0QDW3waEDIcE3isZ79RP6g==}
dev: true
@@ -1653,6 +1882,15 @@
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
dev: true
+ /encode-utf8@1.0.3:
+ resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==}
+ dev: true
+
+ /encodeurl@1.0.2:
+ resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
/encoding@0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
requiresBuild: true
@@ -1667,6 +1905,31 @@
once: 1.4.0
dev: true
+ /engine.io-parser@5.2.2:
+ resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==}
+ engines: {node: '>=10.0.0'}
+ dev: true
+
+ /engine.io@6.5.4:
+ resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==}
+ engines: {node: '>=10.2.0'}
+ dependencies:
+ '@types/cookie': 0.4.1
+ '@types/cors': 2.8.17
+ '@types/node': 20.11.19
+ accepts: 1.3.8
+ base64id: 2.0.0
+ cookie: 0.4.2
+ cors: 2.8.5
+ debug: 4.3.4(supports-color@8.1.1)
+ engine.io-parser: 5.2.2
+ ws: 8.11.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: true
+
/enquirer@2.3.6:
resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
engines: {node: '>=8.6'}
@@ -1674,6 +1937,10 @@
ansi-colors: 4.1.3
dev: true
+ /ent@2.2.0:
+ resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==}
+ dev: true
+
/env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -1688,6 +1955,10 @@
engines: {node: '>=6'}
dev: true
+ /escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+ dev: true
+
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
@@ -1701,6 +1972,10 @@
resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
dev: true
+ /eventemitter3@4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+ dev: true
+
/execa@4.1.0:
resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
engines: {node: '>=10'}
@@ -1783,6 +2058,43 @@
to-regex-range: 5.0.1
dev: true
+ /finalhandler@1.1.2:
+ resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ debug: 2.6.9
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ on-finished: 2.3.0
+ parseurl: 1.3.3
+ statuses: 1.5.0
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+ dev: true
+
+ /flatted@3.3.1:
+ resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
+ dev: true
+
+ /follow-redirects@1.15.5:
+ resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+ dev: true
+
/foreground-child@3.1.1:
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
engines: {node: '>=14'}
@@ -1804,6 +2116,15 @@
mime-types: 2.1.35
dev: true
+ /fs-extra@8.1.0:
+ resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
+ engines: {node: '>=6 <7 || >=8'}
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 4.0.0
+ universalify: 0.1.2
+ dev: true
+
/fs-extra@9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
@@ -1879,7 +2200,7 @@
/get-intrinsic@1.2.1:
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
dependencies:
- function-bind: 1.1.1
+ function-bind: 1.1.2
has: 1.0.3
has-proto: 1.0.1
has-symbols: 1.0.3
@@ -2018,6 +2339,17 @@
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
dev: true
+ /http-errors@2.0.0:
+ resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.1
+ toidentifier: 1.0.1
+ dev: true
+
/http-proxy-agent@5.0.0:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'}
@@ -2029,6 +2361,17 @@
- supports-color
dev: true
+ /http-proxy@1.18.1:
+ resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
+ engines: {node: '>=8.0.0'}
+ dependencies:
+ eventemitter3: 4.0.7
+ follow-redirects: 1.15.5
+ requires-port: 1.0.0
+ transitivePeerDependencies:
+ - debug
+ dev: true
+
/http-signature@1.3.6:
resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==}
engines: {node: '>=0.10'}
@@ -2254,6 +2597,11 @@
is-docker: 2.2.1
dev: true
+ /isbinaryfile@4.0.10:
+ resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
+ engines: {node: '>= 8.0.0'}
+ dev: true
+
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
@@ -2312,6 +2660,12 @@
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: true
+ /jsonfile@4.0.0:
+ resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
+ optionalDependencies:
+ graceful-fs: 4.2.11
+ dev: true
+
/jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
@@ -2335,6 +2689,50 @@
verror: 1.10.0
dev: true
+ /karma-safari-launcher@1.0.0(karma@6.4.3):
+ resolution: {integrity: sha512-qmypLWd6F2qrDJfAETvXDfxHvKDk+nyIjpH9xIeI3/hENr0U3nuqkxaftq73PfXZ4aOuOChA6SnLW4m4AxfRjQ==}
+ peerDependencies:
+ karma: '>=0.9'
+ dependencies:
+ karma: 6.4.3
+ dev: true
+
+ /karma@6.4.3:
+ resolution: {integrity: sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==}
+ engines: {node: '>= 10'}
+ hasBin: true
+ dependencies:
+ '@colors/colors': 1.5.0
+ body-parser: 1.20.2
+ braces: 3.0.2
+ chokidar: 3.5.3
+ connect: 3.7.0
+ di: 0.0.1
+ dom-serialize: 2.2.1
+ glob: 7.2.3
+ graceful-fs: 4.2.11
+ http-proxy: 1.18.1
+ isbinaryfile: 4.0.10
+ lodash: 4.17.21
+ log4js: 6.9.1
+ mime: 2.6.0
+ minimatch: 3.1.2
+ mkdirp: 0.5.6
+ qjobs: 1.2.0
+ range-parser: 1.2.1
+ rimraf: 3.0.2
+ socket.io: 4.7.4
+ source-map: 0.6.1
+ tmp: 0.2.1
+ ua-parser-js: 0.7.37
+ yargs: 16.2.0
+ transitivePeerDependencies:
+ - bufferutil
+ - debug
+ - supports-color
+ - utf-8-validate
+ dev: true
+
/lazy-ass@1.6.0:
resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==}
engines: {node: '> 0.8'}
@@ -2360,6 +2758,13 @@
wrap-ansi: 7.0.0
dev: true
+ /locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+ dependencies:
+ p-locate: 4.1.0
+ dev: true
+
/lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
dev: true
@@ -2386,6 +2791,19 @@
wrap-ansi: 6.2.0
dev: true
+ /log4js@6.9.1:
+ resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ date-format: 4.0.14
+ debug: 4.3.4(supports-color@8.1.1)
+ flatted: 3.3.1
+ rfdc: 1.3.0
+ streamroller: 3.1.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/lru-cache@10.2.0:
resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
engines: {node: 14 || >=16.14}
@@ -2472,6 +2890,11 @@
- supports-color
dev: true
+ /media-typer@0.3.0:
+ resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
/merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
@@ -2488,6 +2911,12 @@
mime-db: 1.52.0
dev: true
+ /mime@2.6.0:
+ resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
+ engines: {node: '>=4.0.0'}
+ hasBin: true
+ dev: true
+
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@@ -2599,12 +3028,23 @@
yallist: 4.0.0
dev: true
+ /mkdirp@0.5.6:
+ resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+ hasBin: true
+ dependencies:
+ minimist: 1.2.8
+ dev: true
+
/mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
hasBin: true
dev: true
+ /ms@2.0.0:
+ resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+ dev: true
+
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
@@ -2748,10 +3188,29 @@
set-blocking: 2.0.0
dev: true
+ /object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
/object-inspect@1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
dev: true
+ /on-finished@2.3.0:
+ resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ ee-first: 1.1.1
+ dev: true
+
+ /on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ ee-first: 1.1.1
+ dev: true
+
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
@@ -2798,6 +3257,20 @@
resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
dev: true
+ /p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+ dependencies:
+ p-try: 2.2.0
+ dev: true
+
+ /p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+ dependencies:
+ p-limit: 2.3.0
+ dev: true
+
/p-map@4.0.0:
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
engines: {node: '>=10'}
@@ -2805,6 +3278,11 @@
aggregate-error: 3.1.0
dev: true
+ /p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+ dev: true
+
/pacote@15.2.0:
resolution: {integrity: sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -2833,10 +3311,23 @@
- supports-color
dev: true
+ /pako@2.1.0:
+ resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
+
/parse5@6.0.1:
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
dev: true
+ /parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
+ /path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+ dev: true
+
/path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
@@ -2886,6 +3377,11 @@
engines: {node: '>=6'}
dev: true
+ /pngjs@5.0.0:
+ resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
+ engines: {node: '>=10.13.0'}
+ dev: true
+
/prettier@2.6.1:
resolution: {integrity: sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==}
engines: {node: '>=10.13.0'}
@@ -2902,6 +3398,11 @@
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dev: true
+ /process@0.11.10:
+ resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
+ engines: {node: '>= 0.6.0'}
+ dev: true
+
/promise-inflight@1.0.1:
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
peerDependencies:
@@ -2939,6 +3440,22 @@
engines: {node: '>=6'}
dev: true
+ /qjobs@1.2.0:
+ resolution: {integrity: sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==}
+ engines: {node: '>=0.9'}
+ dev: true
+
+ /qrcode@1.5.3:
+ resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+ dependencies:
+ dijkstrajs: 1.0.3
+ encode-utf8: 1.0.3
+ pngjs: 5.0.0
+ yargs: 15.4.1
+ dev: true
+
/qs@6.10.4:
resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==}
engines: {node: '>=0.6'}
@@ -2946,6 +3463,32 @@
side-channel: 1.0.4
dev: true
+ /qs@6.11.0:
+ resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
+ engines: {node: '>=0.6'}
+ dependencies:
+ side-channel: 1.0.4
+ dev: true
+
+ /querystringify@2.2.0:
+ resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
+ dev: true
+
+ /range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
+ /raw-body@2.5.2:
+ resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.0
+ iconv-lite: 0.4.24
+ unpipe: 1.0.0
+ dev: true
+
/read-package-json-fast@3.0.2:
resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -3000,12 +3543,20 @@
engines: {node: '>=0.10.0'}
dev: true
+ /require-main-filename@2.0.0:
+ resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
+ dev: true
+
/requirejs@2.3.6:
resolution: {integrity: sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
+ /requires-port@1.0.0:
+ resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+ dev: true
+
/resolve@1.22.2:
resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==}
hasBin: true
@@ -3096,14 +3647,6 @@
hasBin: true
dev: true
- /semver@7.5.3:
- resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==}
- engines: {node: '>=10'}
- hasBin: true
- dependencies:
- lru-cache: 6.0.0
- dev: true
-
/semver@7.5.4:
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
engines: {node: '>=10'}
@@ -3124,6 +3667,10 @@
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: true
+ /setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+ dev: true
+
/shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -3195,6 +3742,44 @@
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
dev: true
+ /socket.io-adapter@2.5.4:
+ resolution: {integrity: sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==}
+ dependencies:
+ debug: 4.3.4(supports-color@8.1.1)
+ ws: 8.11.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: true
+
+ /socket.io-parser@4.2.4:
+ resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
+ engines: {node: '>=10.0.0'}
+ dependencies:
+ '@socket.io/component-emitter': 3.1.0
+ debug: 4.3.4(supports-color@8.1.1)
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /socket.io@4.7.4:
+ resolution: {integrity: sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==}
+ engines: {node: '>=10.2.0'}
+ dependencies:
+ accepts: 1.3.8
+ base64id: 2.0.0
+ cors: 2.8.5
+ debug: 4.3.4(supports-color@8.1.1)
+ engine.io: 6.5.4
+ socket.io-adapter: 2.5.4
+ socket.io-parser: 4.2.4
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: true
+
/socks-proxy-agent@7.0.0:
resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==}
engines: {node: '>= 10'}
@@ -3283,6 +3868,27 @@
minipass: 3.3.6
dev: true
+ /statuses@1.5.0:
+ resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
+ /statuses@2.0.1:
+ resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
+ /streamroller@3.1.5:
+ resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ date-format: 4.0.14
+ debug: 4.3.4(supports-color@8.1.1)
+ fs-extra: 8.1.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -3414,12 +4020,19 @@
is-number: 7.0.0
dev: true
- /tough-cookie@2.5.0:
- resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
- engines: {node: '>=0.8'}
+ /toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+ dev: true
+
+ /tough-cookie@4.1.3:
+ resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
+ engines: {node: '>=6'}
dependencies:
psl: 1.9.0
punycode: 2.3.0
+ universalify: 0.2.0
+ url-parse: 1.5.10
dev: true
/tslib@2.6.0:
@@ -3451,12 +4064,24 @@
engines: {node: '>=10'}
dev: true
+ /type-is@1.6.18:
+ resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ media-typer: 0.3.0
+ mime-types: 2.1.35
+ dev: true
+
/typescript@5.1.6:
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
engines: {node: '>=14.17'}
hasBin: true
dev: true
+ /ua-parser-js@0.7.37:
+ resolution: {integrity: sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==}
+ dev: true
+
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true
@@ -3489,11 +4114,26 @@
imurmurhash: 0.1.4
dev: true
+ /universalify@0.1.2:
+ resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
+ engines: {node: '>= 4.0.0'}
+ dev: true
+
+ /universalify@0.2.0:
+ resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
+ engines: {node: '>= 4.0.0'}
+ dev: true
+
/universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
dev: true
+ /unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
/untildify@4.0.0:
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
engines: {node: '>=8'}
@@ -3516,10 +4156,22 @@
punycode: 2.3.0
dev: true
+ /url-parse@1.5.10:
+ resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
+ dependencies:
+ querystringify: 2.2.0
+ requires-port: 1.0.0
+ dev: true
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
+ /utils-merge@1.0.1:
+ resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
+ engines: {node: '>= 0.4.0'}
+ dev: true
+
/uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
@@ -3539,6 +4191,11 @@
builtins: 5.0.1
dev: true
+ /vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
/verror@1.10.0:
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
engines: {'0': node >=0.6.0}
@@ -3548,12 +4205,21 @@
extsprintf: 1.3.0
dev: true
+ /void-elements@2.0.1:
+ resolution: {integrity: sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
/wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
dependencies:
defaults: 1.0.4
dev: true
+ /which-module@2.0.1:
+ resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
+ dev: true
+
/which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -3607,6 +4273,23 @@
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true
+ /ws@8.11.0:
+ resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: ^5.0.2
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+ dev: true
+
+ /y18n@4.0.3:
+ resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+ dev: true
+
/y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -3620,11 +4303,54 @@
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: true
+ /yargs-parser@18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+ dev: true
+
+ /yargs-parser@20.2.9:
+ resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
+ engines: {node: '>=10'}
+ dev: true
+
/yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
dev: true
+ /yargs@15.4.1:
+ resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+ engines: {node: '>=8'}
+ dependencies:
+ cliui: 6.0.0
+ decamelize: 1.2.0
+ find-up: 4.1.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 4.2.3
+ which-module: 2.0.1
+ y18n: 4.0.3
+ yargs-parser: 18.1.3
+ dev: true
+
+ /yargs@16.2.0:
+ resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
+ engines: {node: '>=10'}
+ dependencies:
+ cliui: 7.0.4
+ escalade: 3.1.1
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 20.2.9
+ dev: true
+
/yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
diff --git a/scouting/BUILD b/scouting/BUILD
index 82f058c..ba6ab6b 100644
--- a/scouting/BUILD
+++ b/scouting/BUILD
@@ -38,6 +38,22 @@
],
)
+# The QR code test is separate from scouting_test because it's slow. Most of
+# the time folks will want to iterate on `scouting_test`.
+cypress_test(
+ name = "scouting_qrcode_test",
+ size = "large",
+ data = [
+ "scouting_qrcode_test.cy.js",
+ "//scouting/testing:scouting_test_servers",
+ "//scouting/testing/camera_simulator",
+ ],
+ runner = "scouting_test_runner.js",
+ tags = [
+ "no-remote-cache",
+ ],
+)
+
apache_wrapper(
name = "https",
binary = ":scouting",
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 5d7a56c..016ca95 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -86,6 +86,7 @@
NotesDropped int32
Penalties int32
AvgCycle int64
+ RobotDied bool
Park, OnStage, Harmony, TrapNote, Spotlight bool
// The username of the person who collected these statistics.
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index 369ea58..882dfbc 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -149,7 +149,7 @@
SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
NotesDropped: 0, Penalties: 2, TrapNote: true, Spotlight: false, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "emma",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "emma",
},
Stats2024{
PreScouting: false, TeamNumber: "942",
@@ -157,7 +157,7 @@
SpeakerAuto: 2, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
NotesDropped: 0, Penalties: 2, TrapNote: true, Spotlight: false, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "harry",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "harry",
},
Stats2024{
PreScouting: false, TeamNumber: "432",
@@ -165,7 +165,7 @@
SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 2, Amp: 1, SpeakerAmplified: 3, AmpAmplified: 0,
NotesDropped: 0, Penalties: 0, TrapNote: false, Spotlight: false, AvgCycle: 0,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "henry",
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "henry",
},
Stats2024{
PreScouting: false, TeamNumber: "52A",
@@ -173,7 +173,7 @@
SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 0, Amp: 1, SpeakerAmplified: 2, AmpAmplified: 3,
NotesDropped: 2, Penalties: 0, TrapNote: true, Spotlight: false, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "jordan",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "jordan",
},
Stats2024{
PreScouting: false, TeamNumber: "745",
@@ -181,7 +181,7 @@
SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 5, Amp: 0, SpeakerAmplified: 2, AmpAmplified: 1,
NotesDropped: 1, Penalties: 1, TrapNote: true, Spotlight: true, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "taylor",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "taylor",
},
Stats2024{
PreScouting: false, TeamNumber: "934",
@@ -189,7 +189,7 @@
SpeakerAuto: 1, AmpAuto: 3, NotesDroppedAuto: 0, MobilityAuto: true,
Speaker: 0, Amp: 3, SpeakerAmplified: 2, AmpAmplified: 2,
NotesDropped: 0, Penalties: 3, TrapNote: true, Spotlight: false, AvgCycle: 0,
- Park: false, OnStage: false, Harmony: true, CollectedBy: "katie",
+ Park: false, OnStage: false, Harmony: true, RobotDied: false, CollectedBy: "katie",
},
}
@@ -236,7 +236,7 @@
SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
NotesDropped: 0, Penalties: 2, TrapNote: true, Spotlight: true, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "emma",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "emma",
}
// Attempt to insert the non-pre-scouted data and make sure it fails.
@@ -265,7 +265,7 @@
SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
NotesDropped: 0, Penalties: 2, TrapNote: true, Spotlight: true, AvgCycle: 0,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "emma",
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "emma",
},
Stats2024{
PreScouting: false, TeamNumber: "978",
@@ -273,7 +273,7 @@
SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 1, Amp: 2, SpeakerAmplified: 0, AmpAmplified: 2,
NotesDropped: 0, Penalties: 2, TrapNote: true, Spotlight: true, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "emma",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "emma",
},
Stats2024{
PreScouting: false, TeamNumber: "328A",
@@ -281,7 +281,7 @@
SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 1, MobilityAuto: true,
Speaker: 0, Amp: 1, SpeakerAmplified: 1, AmpAmplified: 5,
NotesDropped: 1, Penalties: 0, TrapNote: false, Spotlight: true, AvgCycle: 0,
- Park: false, OnStage: false, Harmony: true, CollectedBy: "emma",
+ Park: false, OnStage: false, Harmony: true, RobotDied: true, CollectedBy: "emma",
},
}
@@ -700,7 +700,7 @@
SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 1, Amp: 3, SpeakerAmplified: 1, AmpAmplified: 3,
NotesDropped: 0, Penalties: 0, TrapNote: false, Spotlight: false, AvgCycle: 0,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "bailey",
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "bailey",
},
Stats2024{
PreScouting: false, TeamNumber: "645",
@@ -708,7 +708,7 @@
SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 1, Amp: 2, SpeakerAmplified: 0, AmpAmplified: 1,
NotesDropped: 0, Penalties: 2, TrapNote: true, Spotlight: true, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "kate",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "kate",
},
Stats2024{
PreScouting: false, TeamNumber: "323",
@@ -716,7 +716,7 @@
SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 1, MobilityAuto: true,
Speaker: 0, Amp: 0, SpeakerAmplified: 2, AmpAmplified: 1,
NotesDropped: 1, Penalties: 0, TrapNote: false, Spotlight: false, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "tyler",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "tyler",
},
Stats2024{
PreScouting: false, TeamNumber: "542",
@@ -724,7 +724,7 @@
SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 1, Amp: 2, SpeakerAmplified: 2, AmpAmplified: 1,
NotesDropped: 1, Penalties: 0, TrapNote: false, Spotlight: false, AvgCycle: 0,
- Park: false, OnStage: false, Harmony: true, CollectedBy: "max",
+ Park: false, OnStage: false, Harmony: true, RobotDied: false, CollectedBy: "max",
},
}
@@ -735,7 +735,7 @@
SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 1, Amp: 3, SpeakerAmplified: 1, AmpAmplified: 3,
NotesDropped: 0, Penalties: 0, TrapNote: false, Spotlight: false, AvgCycle: 0,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "bailey",
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "bailey",
},
}
@@ -1154,7 +1154,7 @@
SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
NotesDropped: 0, Penalties: 2, TrapNote: true, Spotlight: false, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "emma",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "emma",
},
Stats2024{
PreScouting: false, TeamNumber: "942",
@@ -1162,7 +1162,7 @@
SpeakerAuto: 2, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 0, Amp: 5, SpeakerAmplified: 1, AmpAmplified: 0,
NotesDropped: 0, Penalties: 2, TrapNote: true, Spotlight: false, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "harry",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "harry",
},
Stats2024{
PreScouting: false, TeamNumber: "432",
@@ -1170,7 +1170,7 @@
SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 2, MobilityAuto: true,
Speaker: 2, Amp: 1, SpeakerAmplified: 3, AmpAmplified: 0,
NotesDropped: 0, Penalties: 0, TrapNote: false, Spotlight: false, AvgCycle: 0,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "henry",
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "henry",
},
Stats2024{
PreScouting: false, TeamNumber: "52A",
@@ -1178,7 +1178,7 @@
SpeakerAuto: 1, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 0, Amp: 1, SpeakerAmplified: 2, AmpAmplified: 3,
NotesDropped: 2, Penalties: 0, TrapNote: true, Spotlight: true, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "jordan",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "jordan",
},
}
diff --git a/scouting/scouting_qrcode_test.cy.js b/scouting/scouting_qrcode_test.cy.js
new file mode 100644
index 0000000..668cba8
--- /dev/null
+++ b/scouting/scouting_qrcode_test.cy.js
@@ -0,0 +1,157 @@
+/// <reference types="cypress" />
+
+// On the 3rd row of matches (index 2) click on the third team
+// (index 2) which resolves to team 333 in quals match 3.
+const QUALS_MATCH_3_TEAM_333 = 2 * 6 + 2;
+
+function disableAlerts() {
+ cy.get('#block_alerts').check({force: true}).should('be.checked');
+}
+
+function switchToTab(tabName) {
+ cy.contains('.nav-link', tabName).click();
+}
+
+function headerShouldBe(text) {
+ cy.get('.header').should('have.text', text);
+}
+
+function clickButton(buttonName) {
+ cy.contains('button', buttonName).click();
+}
+
+// Wrapper around cy.exec() because it truncates the output of the subprocess
+// if it fails. This is a work around to manually print the full error on the
+// console if a failure happends.
+function exec(command) {
+ cy.exec(command, {failOnNonZeroExit: false}).then((result) => {
+ if (result.code) {
+ throw new Error(`Execution of "${command}" failed
+ Exit code: ${result.code}
+ Stdout:\n${result.stdout}
+ Stderr:\n${result.stderr}`);
+ }
+ });
+}
+
+// Prepares data entry so that we _could_ hit Submit.
+//
+// Options:
+// matchButtonKey: The index into the big matchlist table that we want to
+// click on to start the data entry.
+// teamNumber: The team number that matches the button that we click on as
+// specified by `matchButtonKey`.
+//
+// TODO(phil): Deduplicate with scouting_test.cy.js.
+function prepareDataScouting(options) {
+ const {matchButtonKey = SEMI_FINAL_2_MATCH_3_TEAM_5254, teamNumber = 5254} =
+ options;
+
+ // Click on a random team in the Match list. The exact details here are not
+ // important, but we need to know what they are. This could as well be any
+ // other team from any other match.
+ cy.get('button.match-item').eq(matchButtonKey).click();
+
+ // Select Starting Position.
+ headerShouldBe(teamNumber + ' Init ');
+ cy.get('[type="radio"]').first().check();
+ clickButton('Start Match');
+
+ // Pick and Place Note in Auto.
+ clickButton('NOTE');
+ clickButton('AMP');
+
+ // Pick and Place Cube in Teleop.
+ clickButton('Start Teleop');
+ clickButton('NOTE');
+ clickButton('AMP AMPLIFIED');
+
+ // Generate some extra actions so that we are guaranteed to have at least 2
+ // QR codes.
+ for (let i = 0; i < 5; i++) {
+ clickButton('NOTE');
+ clickButton('AMP');
+ }
+
+ // Robot dead and revive.
+ clickButton('DEAD');
+ clickButton('Revive');
+
+ // Endgame.
+ clickButton('Endgame');
+ cy.contains(/Harmony/).click();
+
+ clickButton('End Match');
+ headerShouldBe(teamNumber + ' Review and Submit ');
+ cy.get('#review_data li')
+ .eq(0)
+ .should('have.text', ' Started match at position 1 ');
+ cy.get('#review_data li').eq(1).should('have.text', 'Picked up Note');
+ cy.get('#review_data li')
+ .last()
+ .should(
+ 'have.text',
+ ' Ended Match; stageType: kHARMONY, trapNote: false, spotlight: false '
+ );
+}
+
+before(() => {
+ cy.visit('/');
+ disableAlerts();
+ cy.title().should('eq', 'FRC971 Scouting Application');
+});
+
+beforeEach(() => {
+ cy.visit('/');
+ disableAlerts();
+});
+
+describe('Scouting app tests', () => {
+ // This test collects some scouting data and then generates the corresponding
+ // QR codes. The test takes screenshots of those QR codes. The QR codes get
+ // turned into a little video file for the browser to use as a fake camera
+ // input. The test then switches to the Scan tab to scan the QR codes from
+ // the "camera". We then make sure that the data gets submitted.
+ it('should: be able to generate and scan QR codes.', () => {
+ prepareDataScouting({
+ matchButtonKey: QUALS_MATCH_3_TEAM_333,
+ teamNumber: 333,
+ });
+ clickButton('Create QR Code');
+ headerShouldBe('333 QR Code ');
+
+ cy.get('#qr_code_piece_size').select('150');
+
+ // Go into a mobile-phone view so that we can guarantee that the QR code is
+ // visible.
+ cy.viewport(400, 660);
+
+ cy.get('.qrcode-buttons > li > a')
+ .should('have.length.at.least', 4)
+ .each(($button, index, $buttons) => {
+ if (index == 0 || index + 1 == $buttons.length) {
+ // Skip the "Previous" and "Next" buttons.
+ return;
+ }
+ // Click on the button to switch to that particular QR code.
+ // We use force:true here because without bootstrap (inside the
+ // sandbox) the buttons overlap one another a bit.
+ cy.wrap($button).click({force: true});
+ cy.get('div.qrcode').screenshot(`qrcode_${index}_screenshot`);
+ });
+
+ exec('./testing/camera_simulator/camera_simulator_/camera_simulator');
+
+ switchToTab('Scan');
+
+ // Since we cannot reliably predict how long it will take to scan all the
+ // QR codes, we use a really long timeout here.
+ cy.get('.progress_message', {timeout: 80000}).should('contain', 'Success!');
+
+ // Now that the data is submitted, the button should be disabled.
+ switchToTab('Match List');
+ cy.get('button.match-item')
+ .eq(QUALS_MATCH_3_TEAM_333)
+ .should('be.disabled');
+ });
+});
diff --git a/scouting/scouting_test.cy.js b/scouting/scouting_test.cy.js
index 31ca935..d15cdfc 100644
--- a/scouting/scouting_test.cy.js
+++ b/scouting/scouting_test.cy.js
@@ -117,15 +117,27 @@
clickButton('Submit');
headerShouldBe(teamNumber + ' Success ');
}
+function visit(path) {
+ cy.visit(path, {
+ onBeforeLoad(win) {
+ // The service worker seems to interfere with Cypress somehow. There
+ // doesn't seem to be a proper fix for this issue. Work around it with
+ // this hack that disables the service worker.
+ // https://github.com/cypress-io/cypress/issues/16192#issuecomment-870421667
+ // https://github.com/cypress-io/cypress/issues/702#issuecomment-587127275
+ delete win.navigator.__proto__.serviceWorker;
+ },
+ });
+}
before(() => {
- cy.visit('/');
+ visit('/');
disableAlerts();
cy.title().should('eq', 'FRC971 Scouting Application');
});
beforeEach(() => {
- cy.visit('/');
+ visit('/');
disableAlerts();
});
diff --git a/scouting/testing/camera_simulator/BUILD b/scouting/testing/camera_simulator/BUILD
new file mode 100644
index 0000000..b6e4bf3
--- /dev/null
+++ b/scouting/testing/camera_simulator/BUILD
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "camera_simulator_lib",
+ srcs = ["camera_simulator.go"],
+ importpath = "github.com/frc971/971-Robot-Code/scouting/testing/camera_simulator",
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ visibility = ["//visibility:private"],
+)
+
+go_binary(
+ name = "camera_simulator",
+ embed = [":camera_simulator_lib"],
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ visibility = ["//visibility:public"],
+)
diff --git a/scouting/testing/camera_simulator/camera_simulator.go b/scouting/testing/camera_simulator/camera_simulator.go
new file mode 100644
index 0000000..a54e79f
--- /dev/null
+++ b/scouting/testing/camera_simulator/camera_simulator.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+ "image/jpeg"
+ _ "image/png"
+ "log"
+ "os"
+ "path/filepath"
+ "sort"
+)
+
+// Chrome plays back MJPEG files at a (hard-coded) 30 fps.
+const CHROME_FAKE_VIDEO_FPS = 30
+
+// For how many seconds to display a single image.
+const IMAGE_DURATION = 3
+
+// For how many frames (at CHROME_FAKE_VIDEO_FPS) to display a single image.
+const IMAGE_DURATION_FRAMES = int(CHROME_FAKE_VIDEO_FPS * IMAGE_DURATION)
+
+func checkErr(err error, message string) {
+ if err != nil {
+ log.Println(message)
+ log.Fatal(err)
+ }
+}
+
+func main() {
+ output_dir := os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR")
+
+ // The output file is at a fixed path as expected by
+ // `tools/build_rules/js/cypress.config.js`.
+ outName := output_dir + "/fake_camera.mjpeg"
+
+ // The Cypress test is expected to dump all the screenshots in this
+ // directory.
+ screenshotDir := output_dir + "/screenshots/scouting_qrcode_test.cy.js"
+ log.Printf("Looking for screenshots in %s", screenshotDir)
+
+ // Create a movie from images.
+ matches, err := filepath.Glob(screenshotDir + "/qrcode_*_screenshot.png")
+ checkErr(err, "Failed to glob for the screenshots")
+ sort.Strings(matches)
+
+ log.Println("Found images:", matches)
+ if len(matches) < 2 {
+ // For the purposes of the test, we expect at least 2 QR codes.
+ // If something goes wrong, then this is an opportunity to bail
+ // early.
+ log.Fatalf("Only found %d images", len(matches))
+ }
+
+ mjpeg, err := os.Create(outName)
+ checkErr(err, "Failed to open output file")
+ defer mjpeg.Close()
+
+ // MJPEG is litterally a bunch of JPEGs concatenated together. Read in
+ // each frame and append it to the output file.
+ for _, name := range matches {
+ reader, err := os.Open(name)
+ checkErr(err, "Could not open "+name)
+ defer reader.Close()
+
+ img, _, err := image.Decode(reader)
+ checkErr(err, "Could not decode image")
+
+ buffer := &bytes.Buffer{}
+ checkErr(jpeg.Encode(buffer, img, nil), "Failed to encode as jpeg")
+
+ // In order to show a single picture for 1 second, we need to
+ // inject CHROME_FAKE_VIDEO_FPS copies of the same image.
+ for i := 0; i < IMAGE_DURATION_FRAMES; i++ {
+ _, err = mjpeg.Write(buffer.Bytes())
+ checkErr(err, "Failed to write to mjpeg")
+ }
+ }
+
+ fmt.Printf("%s was written successfully.\n", outName)
+}
diff --git a/scouting/webserver/requests/messages/request_2024_data_scouting_response.fbs b/scouting/webserver/requests/messages/request_2024_data_scouting_response.fbs
index c36174d..f450d38 100644
--- a/scouting/webserver/requests/messages/request_2024_data_scouting_response.fbs
+++ b/scouting/webserver/requests/messages/request_2024_data_scouting_response.fbs
@@ -26,6 +26,7 @@
on_stage: bool (id:16);
harmony: bool (id:17);
spotlight: bool (id:22);
+ robot_died: bool (id:23);
pre_scouting:bool (id:20);
collected_by:string (id:21);
diff --git a/scouting/webserver/requests/messages/submit_2024_actions.fbs b/scouting/webserver/requests/messages/submit_2024_actions.fbs
index e85563f..9462fbe 100644
--- a/scouting/webserver/requests/messages/submit_2024_actions.fbs
+++ b/scouting/webserver/requests/messages/submit_2024_actions.fbs
@@ -9,6 +9,7 @@
kAMP_AMPLIFIED,
kSPEAKER,
kSPEAKER_AMPLIFIED,
+ kDROPPED,
}
table MobilityAction {
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 76d632d..7d6e98e 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -452,7 +452,7 @@
PreScouting: submit2024Actions.PreScouting(), TeamNumber: string(submit2024Actions.TeamNumber()), MatchNumber: submit2024Actions.MatchNumber(), SetNumber: submit2024Actions.SetNumber(), CompLevel: string(submit2024Actions.CompLevel()),
StartingQuadrant: 0, SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 0, Amp: 0, SpeakerAmplified: 0, AmpAmplified: 0, NotesDropped: 0, Penalties: 0,
- TrapNote: false, Spotlight: false, AvgCycle: 0, Park: false, OnStage: false, Harmony: false, CollectedBy: "",
+ TrapNote: false, Spotlight: false, AvgCycle: 0, Park: false, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "",
}
// Loop over all actions.
for i := 0; i < submit2024Actions.ActionsListLength(); i++ {
@@ -481,19 +481,15 @@
penaltyAction.Init(actionTable.Bytes, actionTable.Pos)
stat.Penalties += penaltyAction.Penalties()
+ } else if action_type == submit_2024_actions.ActionTypeRobotDeathAction {
+ var robotDeathAction submit_2024_actions.RobotDeathAction
+ robotDeathAction.Init(actionTable.Bytes, actionTable.Pos)
+ stat.RobotDied = true
+
} else if action_type == submit_2024_actions.ActionTypePickupNoteAction {
var pick_up_action submit_2024_actions.PickupNoteAction
pick_up_action.Init(actionTable.Bytes, actionTable.Pos)
- if picked_up == true {
- auto := pick_up_action.Auto()
- if auto == false {
- stat.NotesDropped += 1
- } else {
- stat.NotesDroppedAuto += 1
- }
- } else {
- picked_up = true
- }
+ picked_up = true
} else if action_type == submit_2024_actions.ActionTypePlaceNoteAction {
var place_action submit_2024_actions.PlaceNoteAction
place_action.Init(actionTable.Bytes, actionTable.Pos)
@@ -514,6 +510,10 @@
stat.SpeakerAuto += 1
} else if score_type == submit_2024_actions.ScoreTypekSPEAKER_AMPLIFIED && !auto {
stat.SpeakerAmplified += 1
+ } else if score_type == submit_2024_actions.ScoreTypekDROPPED && auto {
+ stat.NotesDroppedAuto += 1
+ } else if score_type == submit_2024_actions.ScoreTypekDROPPED && !auto {
+ stat.NotesDropped += 1
} else {
return db.Stats2024{}, errors.New(fmt.Sprintf("Got unknown ObjectType/ScoreLevel/Auto combination"))
}
@@ -595,6 +595,7 @@
Park: stat.Park,
OnStage: stat.OnStage,
Harmony: stat.Harmony,
+ RobotDied: stat.RobotDied,
CollectedBy: stat.CollectedBy,
})
}
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index ebe73f3..26fad0f 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -137,8 +137,8 @@
MatchNumber: 1, SetNumber: 1, CompLevel: "qm", StartingQuadrant: 3,
SpeakerAuto: 2, AmpAuto: 4, NotesDroppedAuto: 1, MobilityAuto: true,
Speaker: 0, Amp: 1, SpeakerAmplified: 2, AmpAmplified: 1,
- NotesDropped: 0, Penalties: 01, TrapNote: true, Spotlight: false, AvgCycle: 233,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "alex",
+ NotesDropped: 0, Penalties: 1, TrapNote: true, Spotlight: false, AvgCycle: 233,
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "alex",
},
{
PreScouting: false, TeamNumber: "973",
@@ -146,7 +146,7 @@
SpeakerAuto: 0, AmpAuto: 2, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 0, Amp: 4, SpeakerAmplified: 3, AmpAmplified: 1,
NotesDropped: 0, Penalties: 1, TrapNote: true, Spotlight: false, AvgCycle: 120,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "bob",
+ Park: true, OnStage: false, Harmony: false, RobotDied: true, CollectedBy: "bob",
},
},
}
@@ -215,7 +215,7 @@
SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 0, MobilityAuto: true,
Speaker: 4, Amp: 2, SpeakerAmplified: 1, AmpAmplified: 0,
NotesDropped: 2, Penalties: 2, TrapNote: true, Spotlight: true, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "alex",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "alex",
},
{
PreScouting: false, TeamNumber: "982",
@@ -223,7 +223,7 @@
SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 0, Amp: 2, SpeakerAmplified: 3, AmpAmplified: 2,
NotesDropped: 1, Penalties: 0, TrapNote: false, Spotlight: true, AvgCycle: 0,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "george",
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "george",
},
},
}
@@ -248,7 +248,7 @@
SpeakerAuto: 1, AmpAuto: 1, NotesDroppedAuto: 0, MobilityAuto: true,
Speaker: 4, Amp: 2, SpeakerAmplified: 1, AmpAmplified: 0,
NotesDropped: 2, Penalties: 2, TrapNote: true, Spotlight: true, AvgCycle: 0,
- Park: true, OnStage: false, Harmony: false, CollectedBy: "alex",
+ Park: true, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "alex",
},
{
PreScouting: false, TeamNumber: "982",
@@ -256,7 +256,7 @@
SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 0, Amp: 2, SpeakerAmplified: 3, AmpAmplified: 2,
NotesDropped: 1, Penalties: 0, TrapNote: false, Spotlight: true, AvgCycle: 0,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "george",
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "george",
},
},
}
@@ -441,6 +441,43 @@
Auto: false,
},
},
+ Timestamp: 3200,
+ },
+ {
+ ActionTaken: &submit_2024_actions.ActionTypeT{
+ Type: submit_2024_actions.ActionTypePlaceNoteAction,
+ Value: &submit_2024_actions.PlaceNoteActionT{
+ ScoreType: submit_2024_actions.ScoreTypekDROPPED,
+ Auto: false,
+ },
+ },
+ Timestamp: 3300,
+ },
+ {
+ ActionTaken: &submit_2024_actions.ActionTypeT{
+ Type: submit_2024_actions.ActionTypeRobotDeathAction,
+ Value: &submit_2024_actions.RobotDeathActionT{
+ RobotDead: true,
+ },
+ },
+ Timestamp: 3400,
+ },
+ {
+ ActionTaken: &submit_2024_actions.ActionTypeT{
+ Type: submit_2024_actions.ActionTypeRobotDeathAction,
+ Value: &submit_2024_actions.RobotDeathActionT{
+ RobotDead: false,
+ },
+ },
+ Timestamp: 3450,
+ },
+ {
+ ActionTaken: &submit_2024_actions.ActionTypeT{
+ Type: submit_2024_actions.ActionTypePickupNoteAction,
+ Value: &submit_2024_actions.PickupNoteActionT{
+ Auto: false,
+ },
+ },
Timestamp: 3500,
},
{
@@ -478,10 +515,10 @@
expected := db.Stats2024{
PreScouting: false, TeamNumber: "4244",
MatchNumber: 3, SetNumber: 1, CompLevel: "quals", StartingQuadrant: 2,
- SpeakerAuto: 0, AmpAuto: 1, NotesDroppedAuto: 1, MobilityAuto: true,
+ SpeakerAuto: 0, AmpAuto: 1, NotesDroppedAuto: 0, MobilityAuto: true,
Speaker: 0, Amp: 0, SpeakerAmplified: 1, AmpAmplified: 1,
- NotesDropped: 0, Penalties: 5, TrapNote: false, Spotlight: false, AvgCycle: 950,
- Park: false, OnStage: false, Harmony: true, CollectedBy: "",
+ NotesDropped: 1, Penalties: 5, TrapNote: false, Spotlight: false, AvgCycle: 633,
+ Park: false, OnStage: false, Harmony: true, RobotDied: true, CollectedBy: "",
}
if expected != response {
@@ -1202,7 +1239,7 @@
SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 1, Amp: 0, SpeakerAmplified: 0, AmpAmplified: 0,
NotesDropped: 0, Penalties: 0, TrapNote: false, Spotlight: false, AvgCycle: 0,
- Park: false, OnStage: false, Harmony: false, CollectedBy: "debug_cli",
+ Park: false, OnStage: false, Harmony: false, RobotDied: false, CollectedBy: "debug_cli",
},
}
@@ -1448,7 +1485,7 @@
SpeakerAuto: 0, AmpAuto: 1, NotesDroppedAuto: 1, MobilityAuto: true,
Speaker: 0, Amp: 1, SpeakerAmplified: 1, AmpAmplified: 1,
NotesDropped: 0, Penalties: 1, TrapNote: true, Spotlight: false, AvgCycle: 233,
- Park: false, OnStage: false, Harmony: true, CollectedBy: "alek",
+ Park: false, OnStage: false, Harmony: true, RobotDied: false, CollectedBy: "alek",
},
{
PreScouting: false, TeamNumber: "244",
@@ -1456,7 +1493,7 @@
SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 0, Amp: 0, SpeakerAmplified: 3, AmpAmplified: 1,
NotesDropped: 0, Penalties: 1, TrapNote: false, Spotlight: false, AvgCycle: 120,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "kacey",
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "kacey",
},
},
actions: []db.Action{
@@ -1520,7 +1557,7 @@
SpeakerAuto: 0, AmpAuto: 0, NotesDroppedAuto: 0, MobilityAuto: false,
Speaker: 0, Amp: 0, SpeakerAmplified: 3, AmpAmplified: 1,
NotesDropped: 0, Penalties: 1, TrapNote: false, Spotlight: false, AvgCycle: 120,
- Park: false, OnStage: true, Harmony: false, CollectedBy: "kacey",
+ Park: false, OnStage: true, Harmony: false, RobotDied: false, CollectedBy: "kacey",
},
}
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index a403bf3..a6ca0a1 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -1,25 +1,49 @@
load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file")
+load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_run_binary")
+load("@npm//:@angular/service-worker/package_json.bzl", angular_service_worker = "bin")
load("@npm//:defs.bzl", "npm_link_all_packages")
load("//tools/build_rules:js.bzl", "ng_application")
-load(":defs.bzl", "assemble_static_files")
+load(":defs.bzl", "assemble_service_worker_files", "assemble_static_files")
npm_link_all_packages(name = "node_modules")
+assemble_service_worker_files(
+ name = "service_worker_files",
+ outs = [
+ "ngsw-worker.js",
+ ],
+)
+
+OPENCV_VERSION = "4.9.0"
+
+copy_file(
+ name = "opencv.js",
+ src = "@opencv_wasm//file",
+ out = "assets/opencv_{}/opencv.js".format(OPENCV_VERSION),
+)
+
ng_application(
name = "app",
+ assets = [
+ "manifest.json",
+ ":opencv.js",
+ ],
extra_srcs = [
"app/common.css",
],
html_assets = [
"favicon.ico",
+ "assets/971_144.png",
],
deps = [
"//:node_modules/@angular/animations",
+ "//:node_modules/@angular/service-worker",
"//scouting/www/driver_ranking",
"//scouting/www/entry",
"//scouting/www/match_list",
"//scouting/www/notes",
"//scouting/www/pit_scouting",
+ "//scouting/www/scan",
"//scouting/www/shift_schedule",
"//scouting/www/view",
],
@@ -30,6 +54,8 @@
app_files = ":app",
pictures = [
"//third_party/y2024/field:pictures",
+ ":ngsw-worker.js",
+ ":ngsw.json",
],
replace_prefixes = {
"prod": "",
@@ -48,3 +74,35 @@
out = "app/common.css",
visibility = ["//scouting/www:__subpackages__"],
)
+
+angular_service_worker.ngsw_config_binary(
+ name = "ngsw_config_binary",
+)
+
+js_binary(
+ name = "ngsw_config_wrapper",
+ data = [
+ ":ngsw_config_binary",
+ ],
+ entry_point = "ngsw_config_wrapper.js",
+)
+
+js_run_binary(
+ name = "ngsw_config",
+ srcs = [
+ "manifest.json",
+ "ngsw-config.json",
+ ":app",
+ ":ngsw_config_binary",
+ ],
+ outs = [
+ "ngsw.json",
+ ],
+ args = [
+ "$(rootpath :ngsw_config_binary)",
+ "$(rootpath :ngsw.json)",
+ "$(rootpath :prod)",
+ "$(rootpath ngsw-config.json)",
+ ],
+ tool = ":ngsw_config_wrapper",
+)
diff --git a/scouting/www/app/app.module.ts b/scouting/www/app/app.module.ts
index bf393e4..decd1f3 100644
--- a/scouting/www/app/app.module.ts
+++ b/scouting/www/app/app.module.ts
@@ -1,6 +1,7 @@
-import {NgModule} from '@angular/core';
+import {NgModule, isDevMode} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {ServiceWorkerModule} from '@angular/service-worker';
import {App} from './app';
import {EntryModule} from '../entry';
@@ -10,12 +11,19 @@
import {ViewModule} from '../view';
import {DriverRankingModule} from '../driver_ranking';
import {PitScoutingModule} from '../pit_scouting';
+import {ScanModule} from '../scan';
@NgModule({
declarations: [App],
imports: [
BrowserModule,
BrowserAnimationsModule,
+ ServiceWorkerModule.register('./ngsw-worker.js', {
+ enabled: !isDevMode(),
+ // Register the ServiceWorker as soon as the application is stable
+ // or after 30 seconds (whichever comes first).
+ registrationStrategy: 'registerWhenStable:30000',
+ }),
EntryModule,
NotesModule,
MatchListModule,
@@ -23,6 +31,7 @@
DriverRankingModule,
ViewModule,
PitScoutingModule,
+ ScanModule,
],
exports: [App],
bootstrap: [App],
diff --git a/scouting/www/app/app.ng.html b/scouting/www/app/app.ng.html
index 5f7ea9f..ef37c07 100644
--- a/scouting/www/app/app.ng.html
+++ b/scouting/www/app/app.ng.html
@@ -73,6 +73,15 @@
Pit
</a>
</li>
+ <li class="nav-item">
+ <a
+ class="nav-link"
+ [class.active]="tabIs('Scan')"
+ (click)="switchTabToGuarded('Scan')"
+ >
+ Scan
+ </a>
+ </li>
</ul>
<ng-container [ngSwitch]="tab">
@@ -93,4 +102,5 @@
<shift-schedule *ngSwitchCase="'ShiftSchedule'"></shift-schedule>
<app-view *ngSwitchCase="'View'"></app-view>
<app-pit-scouting *ngSwitchCase="'Pit'"></app-pit-scouting>
+ <app-scan *ngSwitchCase="'Scan'"></app-scan>
</ng-container>
diff --git a/scouting/www/app/app.ts b/scouting/www/app/app.ts
index 597e5c5..ab15ae5 100644
--- a/scouting/www/app/app.ts
+++ b/scouting/www/app/app.ts
@@ -1,4 +1,4 @@
-import {Component, ElementRef, ViewChild} from '@angular/core';
+import {Component, ElementRef, ViewChild, isDevMode} from '@angular/core';
type Tab =
| 'MatchList'
@@ -7,10 +7,11 @@
| 'DriverRanking'
| 'ShiftSchedule'
| 'View'
- | 'Pit';
+ | 'Pit'
+ | 'Scan';
// Ignore the guard for tabs that don't require the user to enter any data.
-const unguardedTabs: Tab[] = ['MatchList', 'View'];
+const unguardedTabs: Tab[] = ['MatchList', 'Scan', 'View'];
type TeamInMatch = {
teamNumber: string;
@@ -39,6 +40,8 @@
@ViewChild('block_alerts') block_alerts: ElementRef;
constructor() {
+ console.log(`Using development mode: ${isDevMode()}`);
+
window.addEventListener('beforeunload', (e) => {
if (!unguardedTabs.includes(this.tab)) {
if (!this.block_alerts.nativeElement.checked) {
diff --git a/scouting/www/assets/971_144.png b/scouting/www/assets/971_144.png
new file mode 100644
index 0000000..881edfa
--- /dev/null
+++ b/scouting/www/assets/971_144.png
Binary files differ
diff --git a/scouting/www/defs.bzl b/scouting/www/defs.bzl
index 828f30a..e7fb44a 100644
--- a/scouting/www/defs.bzl
+++ b/scouting/www/defs.bzl
@@ -27,6 +27,7 @@
),
"pictures": attr.label_list(
mandatory = True,
+ allow_files = True,
),
"replace_prefixes": attr.string_dict(
mandatory = True,
@@ -34,3 +35,45 @@
},
toolchains = ["@aspect_bazel_lib//lib:copy_to_directory_toolchain_type"],
)
+
+def _assemble_service_worker_files_impl(ctx):
+ args = ctx.actions.args()
+ args.add_all(ctx.attr._package.files, before_each = "--input_dir", expand_directories = False)
+ args.add_all(ctx.outputs.outs, before_each = "--output")
+ args.add_all(ctx.attr.outs_as_strings, before_each = "--relative_output")
+ ctx.actions.run(
+ inputs = ctx.attr._package.files,
+ outputs = ctx.outputs.outs,
+ executable = ctx.executable._tool,
+ arguments = [args],
+ mnemonic = "AssembleAngularServiceWorker",
+ )
+
+_assemble_service_worker_files = rule(
+ implementation = _assemble_service_worker_files_impl,
+ attrs = {
+ "outs": attr.output_list(
+ allow_empty = False,
+ mandatory = True,
+ ),
+ "outs_as_strings": attr.string_list(
+ allow_empty = False,
+ mandatory = True,
+ ),
+ "_package": attr.label(
+ default = "//:node_modules/@angular/service-worker",
+ ),
+ "_tool": attr.label(
+ default = "//tools/build_rules/js:assemble_service_worker_files",
+ cfg = "exec",
+ executable = True,
+ ),
+ },
+)
+
+def assemble_service_worker_files(outs, **kwargs):
+ _assemble_service_worker_files(
+ outs = outs,
+ outs_as_strings = outs,
+ **kwargs
+ )
diff --git a/scouting/www/entry/BUILD b/scouting/www/entry/BUILD
index 98b457b..48732f9 100644
--- a/scouting/www/entry/BUILD
+++ b/scouting/www/entry/BUILD
@@ -10,6 +10,10 @@
],
deps = [
":node_modules/@angular/forms",
+ "//:node_modules/@angular/platform-browser",
+ "//:node_modules/@types/pako",
+ "//:node_modules/angularx-qrcode",
+ "//:node_modules/pako",
"//scouting/webserver/requests/messages:error_response_ts_fbs",
"//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
"//scouting/webserver/requests/messages:submit_2024_actions_ts_fbs",
diff --git a/scouting/www/entry/entry.component.css b/scouting/www/entry/entry.component.css
index 9c50ee0..e646a25 100644
--- a/scouting/www/entry/entry.component.css
+++ b/scouting/www/entry/entry.component.css
@@ -20,5 +20,51 @@
}
#EndGame > div > label {
- padding: 0;
+ padding: 0px;
+}
+
+.button_row {
+ display: flex;
+ width: 90vw;
+ justify-content: space-between;
+}
+
+.qr-container {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ padding: 0px;
+}
+
+.qrcode-buttons > li.page-item {
+ padding: 0px;
+}
+
+/* Using deprecated ::ng-deep here, but couldn't find a better way to do it.
+ * The qrcode container generates a canvas without any style markers or
+ * classes. Angular's view encapsulation prevents this style from applying.
+ * Maybe https://developer.mozilla.org/en-US/docs/Web/CSS/::slotted ? */
+:host ::ng-deep .qr-container canvas {
+ /* Make the QR code take up as much space as possible. Can't take up more
+ * than the screen size, however, because you can't scan a QR code while
+ * scrolling. It needs to be fully visibile. */
+ width: 100% !important;
+ height: 100% !important;
+ aspect-ratio: 1 / 1;
+}
+
+/* Make the UI a little more compact. The QR code itself already has a good
+ * amount of margin. */
+
+.qrcode-nav {
+ padding: 0px;
+}
+
+.qrcode-buttons {
+ padding: 0px;
+ margin: 0px;
+}
+
+.qrcode {
+ padding: 0px;
}
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index b5bc51e..09b1a8b 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -26,6 +26,7 @@
} from '../../webserver/requests/messages/submit_2024_actions_generated';
import {Match} from '../../webserver/requests/messages/request_all_matches_response_generated';
import {MatchListRequestor} from '../rpc';
+import * as pako from 'pako';
type Section =
| 'Team Selection'
@@ -35,6 +36,7 @@
| 'Endgame'
| 'Dead'
| 'Review and Submit'
+ | 'QR Code'
| 'Success';
// TODO(phil): Deduplicate with match_list.component.ts.
@@ -50,6 +52,13 @@
f: 'Finals',
};
+// The maximum number of bytes per QR code. The user can adjust this value to
+// make the QR code contain less information, but easier to scan.
+const QR_CODE_PIECE_SIZES = [150, 300, 450, 600, 750, 900];
+
+// The default index into QR_CODE_PIECE_SIZES.
+const DEFAULT_QR_CODE_PIECE_SIZE_INDEX = QR_CODE_PIECE_SIZES.indexOf(750);
+
type ActionT =
| {
type: 'startMatchAction';
@@ -75,7 +84,7 @@
| {
type: 'robotDeathAction';
timestamp?: number;
- robotOn: boolean;
+ robotDead: boolean;
}
| {
type: 'penaltyAction';
@@ -112,6 +121,7 @@
// of radio buttons.
readonly COMP_LEVELS = COMP_LEVELS;
readonly COMP_LEVEL_LABELS = COMP_LEVEL_LABELS;
+ readonly QR_CODE_PIECE_SIZES = QR_CODE_PIECE_SIZES;
readonly ScoreType = ScoreType;
readonly StageType = StageType;
@@ -136,6 +146,16 @@
teamSelectionIsValid = false;
+ // When the user chooses to generate QR codes, we convert the flatbuffer into
+ // a long string. Since we frequently have more data than we can display in a
+ // single QR code, we break the data into multiple QR codes. The data for
+ // each QR code ("pieces") is stored in the `qrCodeValuePieces` list below.
+ // The `qrCodeValueIndex` keeps track of which QR code we're currently
+ // displaying.
+ qrCodeValuePieceSize = QR_CODE_PIECE_SIZES[DEFAULT_QR_CODE_PIECE_SIZE_INDEX];
+ qrCodeValuePieces: string[] = [];
+ qrCodeValueIndex: number = 0;
+
constructor(private readonly matchListRequestor: MatchListRequestor) {}
ngOnInit() {
@@ -219,7 +239,8 @@
}
if (action.type == 'endMatchAction') {
- // endMatchAction occurs at the same time as penaltyAction so add to its timestamp to make it unique.
+ // endMatchAction occurs at the same time as penaltyAction so add to its
+ // timestamp to make it unique.
action.timestamp += 1;
}
@@ -282,6 +303,11 @@
this.errorMessage = '';
this.progressMessage = '';
+ // For the QR code screen, we need to make the value to encode available.
+ if (target == 'QR Code') {
+ this.updateQrCodeValuePieceSize();
+ }
+
this.section = target;
}
@@ -291,7 +317,7 @@
this.header.nativeElement.scrollIntoView();
}
- async submit2024Actions() {
+ createActionsBuffer() {
const builder = new Builder();
const actionOffsets: number[] = [];
@@ -363,7 +389,7 @@
case 'robotDeathAction':
const robotDeathActionOffset =
- RobotDeathAction.createRobotDeathAction(builder, action.robotOn);
+ RobotDeathAction.createRobotDeathAction(builder, action.robotDead);
actionOffset = Action.createAction(
builder,
BigInt(action.timestamp || 0),
@@ -419,10 +445,44 @@
Submit2024Actions.addPreScouting(builder, this.preScouting);
builder.finish(Submit2024Actions.endSubmit2024Actions(builder));
- const buffer = builder.asUint8Array();
+ return builder.asUint8Array();
+ }
+
+ // Same as createActionsBuffer, but encoded as Base64. It's also split into
+ // a number of pieces so that each piece is roughly limited to
+ // `qrCodeValuePieceSize` bytes.
+ createBase64ActionsBuffers(): string[] {
+ const originalBuffer = this.createActionsBuffer();
+ const deflatedData = pako.deflate(originalBuffer, {level: 9});
+
+ const pieceSize = this.qrCodeValuePieceSize;
+ const fullValue = btoa(String.fromCharCode(...deflatedData));
+ const numPieces = Math.ceil(fullValue.length / pieceSize);
+
+ let splitData: string[] = [];
+ for (let i = 0; i < numPieces; i++) {
+ const splitPiece = fullValue.slice(i * pieceSize, (i + 1) * pieceSize);
+ splitData.push(`${i}_${numPieces}_${pieceSize}_${splitPiece}`);
+ }
+ return splitData;
+ }
+
+ setQrCodeValueIndex(index: number) {
+ this.qrCodeValueIndex = Math.max(
+ 0,
+ Math.min(index, this.qrCodeValuePieces.length - 1)
+ );
+ }
+
+ updateQrCodeValuePieceSize() {
+ this.qrCodeValuePieces = this.createBase64ActionsBuffers();
+ this.qrCodeValueIndex = 0;
+ }
+
+ async submit2024Actions() {
const res = await fetch('/requests/submit/submit_2024_actions', {
method: 'POST',
- body: buffer,
+ body: this.createActionsBuffer(),
});
if (res.ok) {
diff --git a/scouting/www/entry/entry.module.ts b/scouting/www/entry/entry.module.ts
index c74d4d0..bcba4ee 100644
--- a/scouting/www/entry/entry.module.ts
+++ b/scouting/www/entry/entry.module.ts
@@ -2,10 +2,11 @@
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {EntryComponent} from './entry.component';
+import {QRCodeModule} from 'angularx-qrcode';
@NgModule({
declarations: [EntryComponent],
exports: [EntryComponent],
- imports: [CommonModule, FormsModule],
+ imports: [CommonModule, FormsModule, QRCodeModule],
})
export class EntryModule {}
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index ea44ad3..c0bba23 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -123,7 +123,7 @@
<button class="btn btn-secondary" (click)="undoLastAction()">UNDO</button>
<button
class="btn btn-danger"
- (click)="changeSectionTo('Dead'); addAction({type: 'robotDeathAction', robotOn: false});"
+ (click)="changeSectionTo('Dead'); addAction({type: 'robotDeathAction', robotDead: true});"
>
DEAD
</button>
@@ -183,15 +183,21 @@
selection and keep all buttons visible without scrolling on most devices.
-->
<div
- [ngClass]="{'d-grid': true, 'gap-3': autoPhase === true, 'gap-5': autoPhase === false}"
+ [ngClass]="{'d-grid': true, 'gap-4': autoPhase === true, 'gap-3': autoPhase === false}"
>
<button class="btn btn-secondary" (click)="undoLastAction()">UNDO</button>
<button
class="btn btn-danger"
- (click)="changeSectionTo('Dead'); addAction({type: 'robotDeathAction', robotOn: false});"
+ (click)="changeSectionTo('Dead'); addAction({type: 'robotDeathAction', robotDead: true});"
>
DEAD
</button>
+ <button
+ class="btn btn-info"
+ (click)="changeSectionTo('Pickup'); addAction({type: 'placeNoteAction', scoreType: ScoreType.kDROPPED});"
+ >
+ Dropped
+ </button>
<div *ngIf="!autoPhase" class="d-grid gap-1" style="padding: 0">
<div
style="
@@ -298,40 +304,54 @@
<button class="btn btn-secondary" (click)="undoLastAction()">UNDO</button>
<button
class="btn btn-danger"
- (click)="changeSectionTo('Dead'); addAction({type: 'robotDeathAction', robotOn: false});"
+ (click)="changeSectionTo('Dead'); addAction({type: 'robotDeathAction', robotDead: true});"
>
DEAD
</button>
- <label>
- <input
- #park
- type="radio"
- id="option1"
- name="endgameaction"
- value="park"
- />
- Park
- </label>
- <label>
- <input
- #onStage
- type="radio"
- id="option2"
- name="endgameaction"
- value="onStage"
- />
- On Stage
- </label>
- <label>
- <input
- #harmony
- type="radio"
- id="option3"
- name="endgameaction"
- value="harmony"
- />
- Harmony
- </label>
+ <div class="button_row">
+ <label>
+ <input
+ #park
+ type="radio"
+ id="option1"
+ name="endgameaction"
+ value="park"
+ />
+ Park
+ </label>
+ <label>
+ <input
+ #onStage
+ type="radio"
+ id="option2"
+ name="endgameaction"
+ value="onStage"
+ />
+ On Stage
+ </label>
+ </div>
+ <div class="button_row">
+ <label>
+ <input
+ #harmony
+ type="radio"
+ id="option3"
+ name="endgameaction"
+ value="harmony"
+ />
+ Harmony
+ </label>
+ <label>
+ <input
+ #na
+ type="radio"
+ id="option2"
+ name="endgameaction"
+ value="na"
+ />
+ N/A
+ </label>
+ </div>
<label>
<input
#trapNote
@@ -352,6 +372,7 @@
/>
Spotlight
</label>
+
<div style="display: flex">
<h5>Penalties :</h5>
<button
@@ -381,13 +402,38 @@
</div>
<div *ngSwitchCase="'Dead'" id="Dead" class="container-fluid">
<h2>Robot is dead</h2>
- <div class="d-grid gap-2">
+ <div class="d-grid gap-3">
+ <button class="btn btn-secondary" (click)="undoLastAction()">UNDO</button>
+ <div style="display: flex">
+ <h5>Penalties :</h5>
+ <button
+ class="btn-light"
+ style="width: 40px; margin-right: 15px"
+ (click)="removePenalty()"
+ >
+ -
+ </button>
+ <p>{{this.penalties}}</p>
+ <button
+ class="btn-light"
+ style="width: 40px; margin-left: 15px"
+ (click)="addPenalty()"
+ >
+ +
+ </button>
+ </div>
<button
class="btn btn-success"
- (click)="changeSectionTo('Pickup'); addAction({type: 'robotDeathAction', robotOn: true}); "
+ (click)="changeSectionTo('Pickup'); addAction({type: 'robotDeathAction', robotDead: false}); "
>
Revive
</button>
+ <button
+ class="btn btn-info"
+ (click)="changeSectionTo('Review and Submit'); addPenalties(); addAction({type: 'endMatchAction', stageType: (park.checked ? StageType.kPARK : onStage.checked ? StageType.kON_STAGE : harmony.checked ? StageType.kHARMONY : StageType.kMISSING), trapNote: trapNote.checked, spotlight: spotlight.checked});"
+ >
+ End Match
+ </button>
</div>
</div>
<div *ngSwitchCase="'Review and Submit'" id="Review" class="container-fluid">
@@ -414,7 +460,7 @@
spotlight: {{action.spotlight}}
</span>
<span *ngSwitchCase="'robotDeathAction'">
- Robot on: {{action.robotOn}}
+ Robot dead: {{action.robotDead}}
</span>
<span *ngSwitchCase="'mobilityAction'">
Mobility: {{action.mobility}}
@@ -429,6 +475,9 @@
</div>
<div class="d-grid gap-5">
<button class="btn btn-secondary" (click)="undoLastAction()">UNDO</button>
+ <button class="btn btn-info" (click)="changeSectionTo('QR Code');">
+ Create QR Code
+ </button>
<button class="btn btn-warning" (click)="submit2024Actions();">
Submit
</button>
@@ -437,6 +486,75 @@
<div *ngSwitchCase="'Success'" id="Success" class="container-fluid">
<h2>Successfully submitted data.</h2>
</div>
+ <div *ngSwitchCase="'QR Code'" id="QR Code" class="container-fluid">
+ <span>Density:</span>
+ <select
+ [(ngModel)]="qrCodeValuePieceSize"
+ (ngModelChange)="updateQrCodeValuePieceSize()"
+ type="number"
+ id="qr_code_piece_size"
+ >
+ <option
+ *ngFor="let pieceSize of QR_CODE_PIECE_SIZES"
+ [ngValue]="pieceSize"
+ >
+ {{pieceSize}}
+ </option>
+ </select>
+ <div class="qr-container">
+ <qrcode
+ [qrdata]="qrCodeValuePieces[qrCodeValueIndex]"
+ [width]="1000"
+ [errorCorrectionLevel]="'M'"
+ [margin]="6"
+ class="qrcode"
+ ></qrcode>
+ </div>
+ <nav class="qrcode-nav">
+ <ul
+ class="qrcode-buttons pagination pagination-lg justify-content-center"
+ >
+ <li class="page-item">
+ <a
+ class="page-link"
+ href="#"
+ aria-label="Previous"
+ (click)="setQrCodeValueIndex(qrCodeValueIndex - 1)"
+ >
+ <span aria-hidden="true">«</span>
+ <span class="visually-hidden">Previous</span>
+ </a>
+ </li>
+ <li *ngFor="let _ of qrCodeValuePieces; index as i" class="page-item">
+ <a
+ class="page-link"
+ href="#"
+ (click)="setQrCodeValueIndex(i)"
+ [class.active]="qrCodeValueIndex == i"
+ >
+ {{i + 1}}
+ </a>
+ </li>
+ <li class="page-item">
+ <a
+ class="page-link"
+ href="#"
+ aria-label="Next"
+ (click)="setQrCodeValueIndex(qrCodeValueIndex + 1)"
+ >
+ <span aria-hidden="true">»</span>
+ <span class="visually-hidden">Next</span>
+ </a>
+ </li>
+ </ul>
+ </nav>
+ <button
+ class="btn btn-secondary"
+ (click)="changeSectionTo('Review and Submit')"
+ >
+ BACK
+ </button>
+ </div>
<span class="progress_message" role="alert">{{ progressMessage }}</span>
<span class="error_message" role="alert">{{ errorMessage }}</span>
diff --git a/scouting/www/entry/package.json b/scouting/www/entry/package.json
index d37ce10..a02c0a6 100644
--- a/scouting/www/entry/package.json
+++ b/scouting/www/entry/package.json
@@ -2,6 +2,8 @@
"name": "@org_frc971/scouting/www/entry",
"private": true,
"dependencies": {
+ "pako": "2.1.0",
+ "@types/pako": "2.0.3",
"@org_frc971/scouting/www/counter_button": "workspace:*",
"@angular/forms": "v16-lts"
}
diff --git a/scouting/www/index.html b/scouting/www/index.html
index 46779cc..821acf2 100644
--- a/scouting/www/index.html
+++ b/scouting/www/index.html
@@ -4,6 +4,8 @@
<meta charset="utf-8" />
<title>FRC971 Scouting Application</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="theme-color" content="#000000" />
+ <link rel="manifest" href="/manifest.json" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
@@ -16,8 +18,17 @@
integrity="d8824f7067cdfea38afec7e9ffaf072125266824206d69ef1f112d72153a505e"
/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
+ <script>
+ // In order to hook into WASM's "finished loading" event, we interact
+ // with the global Module variable. Maybe some day there'll be a better
+ // way to interact with it.
+ var Module = {};
+ </script>
</head>
<body>
<my-app></my-app>
+ <noscript>
+ Please enable JavaScript to continue using this application.
+ </noscript>
</body>
</html>
diff --git a/scouting/www/manifest.json b/scouting/www/manifest.json
new file mode 100644
index 0000000..9366399
--- /dev/null
+++ b/scouting/www/manifest.json
@@ -0,0 +1,17 @@
+{
+ "name": "FRC971 Scouting App",
+ "short_name": "scouting",
+ "theme_color": "#1976d2",
+ "background_color": "#fafafa",
+ "display": "standalone",
+ "scope": "./",
+ "start_url": "./",
+ "icons": [
+ {
+ "src": "assets/971_144.png",
+ "sizes": "144x144",
+ "type": "image/png",
+ "purpose": "maskable any"
+ }
+ ]
+}
diff --git a/scouting/www/ngsw-config.json b/scouting/www/ngsw-config.json
new file mode 100644
index 0000000..78d921d
--- /dev/null
+++ b/scouting/www/ngsw-config.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "./node_modules/@angular/service-worker/config/schema.json",
+ "index": "/index.html",
+ "assetGroups": [
+ {
+ "name": "app",
+ "installMode": "prefetch",
+ "resources": {
+ "files": [
+ "/favicon.ico",
+ "/index.html",
+ "/manifest.json",
+ "/*.css",
+ "/bundle-*/*.js",
+ "/*.js"
+ ]
+ }
+ },
+ {
+ "name": "assets",
+ "installMode": "lazy",
+ "updateMode": "prefetch",
+ "resources": {
+ "files": [
+ "/assets/**",
+ "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
+ ]
+ }
+ }
+ ]
+ }
+
diff --git a/scouting/www/ngsw_config_wrapper.js b/scouting/www/ngsw_config_wrapper.js
new file mode 100644
index 0000000..0bc7f19
--- /dev/null
+++ b/scouting/www/ngsw_config_wrapper.js
@@ -0,0 +1,37 @@
+const fs = require('fs');
+const path = require('path');
+const {spawnSync} = require('child_process');
+
+const output_dir = path.join(
+ process.env.BAZEL_BINDIR,
+ process.env.BAZEL_PACKAGE
+);
+console.log(output_dir);
+console.log(process.argv[2]);
+console.log(process.cwd());
+const ngsw_config = process.argv[2];
+console.log(`Trying to run ${ngsw_config} ${process.argv.slice(4).join(' ')}`);
+const result = spawnSync(ngsw_config, process.argv.slice(4), {
+ stdout: 'inherit',
+ stderr: 'inherit',
+});
+
+if (result.status || result.error || result.signal) {
+ console.log("Failed to run 'ngsw_config'");
+ console.log(`status: ${result.status}`);
+ console.log(`error: ${result.error}`);
+ console.log(`signal: ${result.signal}`);
+ console.log(`stdout: ${result.stdout}`);
+ console.log(`stderr: ${result.stderr}`);
+ process.exit(1);
+}
+
+const currentDirectory = process.cwd();
+
+// Read the contents of the current directory
+console.log(`Contents of the current directory: ${currentDirectory}`);
+fs.readdirSync(currentDirectory).forEach((file) => {
+ console.log(file);
+});
+
+fs.copyFileSync(path.join(process.argv[4], 'ngsw.json'), process.argv[3]);
diff --git a/scouting/www/notes/notes.component.ts b/scouting/www/notes/notes.component.ts
index 3848c22..431ec71 100644
--- a/scouting/www/notes/notes.component.ts
+++ b/scouting/www/notes/notes.component.ts
@@ -56,8 +56,8 @@
const KEYWORD_CHECKBOX_LABELS = {
goodDriving: 'Good Driving',
badDriving: 'Bad Driving',
- solidPlacing: 'Solid Placing',
- sketchyPlacing: 'Sketchy Placing',
+ solidPlacing: 'Solid Shooting',
+ sketchyPlacing: 'Sketchy Shooting',
goodDefense: 'Good Defense',
badDefense: 'Bad Defense',
easilyDefended: 'Easily Defended',
diff --git a/scouting/www/rpc/BUILD b/scouting/www/rpc/BUILD
index d1367ea..592735c 100644
--- a/scouting/www/rpc/BUILD
+++ b/scouting/www/rpc/BUILD
@@ -10,6 +10,7 @@
],
generate_public_api = False,
deps = [
+ "//:node_modules/dexie",
"//scouting/webserver/requests/messages:error_response_ts_fbs",
"//scouting/webserver/requests/messages:request_2024_data_scouting_response_ts_fbs",
"//scouting/webserver/requests/messages:request_2024_data_scouting_ts_fbs",
diff --git a/scouting/www/rpc/db.ts b/scouting/www/rpc/db.ts
new file mode 100644
index 0000000..789ac0e
--- /dev/null
+++ b/scouting/www/rpc/db.ts
@@ -0,0 +1,18 @@
+import Dexie, {Table} from 'dexie';
+
+export interface MatchListData {
+ id?: number;
+ data: Uint8Array;
+}
+
+export class AppDB extends Dexie {
+ matchListData!: Table<MatchListData, number>;
+
+ constructor() {
+ super('ngdexieliveQuery');
+ this.version(1).stores({
+ matchListData: 'id,data',
+ });
+ }
+}
+export const db = new AppDB();
diff --git a/scouting/www/rpc/match_list_requestor.ts b/scouting/www/rpc/match_list_requestor.ts
index fa2dcbd..0a812ce 100644
--- a/scouting/www/rpc/match_list_requestor.ts
+++ b/scouting/www/rpc/match_list_requestor.ts
@@ -6,78 +6,82 @@
Match,
RequestAllMatchesResponse,
} from '../../webserver/requests/messages/request_all_matches_response_generated';
-
+import {db, MatchListData} from './db';
const MATCH_TYPE_ORDERING = ['qm', 'ef', 'qf', 'sf', 'f'];
-
@Injectable({providedIn: 'root'})
export class MatchListRequestor {
async fetchMatchList(): Promise<Match[]> {
const builder = new Builder();
RequestAllMatches.startRequestAllMatches(builder);
builder.finish(RequestAllMatches.endRequestAllMatches(builder));
-
const buffer = builder.asUint8Array();
const res = await fetch('/requests/request/all_matches', {
method: 'POST',
body: buffer,
});
-
if (res.ok) {
const resBuffer = await res.arrayBuffer();
- const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
- const parsedResponse =
- RequestAllMatchesResponse.getRootAsRequestAllMatchesResponse(fbBuffer);
-
- // Convert the flatbuffer list into an array. That's more useful.
- const matchList = [];
- for (let i = 0; i < parsedResponse.matchListLength(); i++) {
- matchList.push(parsedResponse.matchList(i));
- }
-
- // Sort the list so it is in chronological order.
- matchList.sort((a, b) => {
- // First sort by match type. E.g. finals are last.
- const aMatchTypeIndex = MATCH_TYPE_ORDERING.indexOf(a.compLevel());
- const bMatchTypeIndex = MATCH_TYPE_ORDERING.indexOf(b.compLevel());
- if (aMatchTypeIndex < bMatchTypeIndex) {
- return -1;
- }
- if (aMatchTypeIndex > bMatchTypeIndex) {
- return 1;
- }
- // Then sort by match number. E.g. in semi finals, all match 1 rounds
- // are done first. Then come match 2 rounds. And then, if necessary,
- // the match 3 rounds.
- const aMatchNumber = a.matchNumber();
- const bMatchNumber = b.matchNumber();
- if (aMatchNumber < bMatchNumber) {
- return -1;
- }
- if (aMatchNumber > bMatchNumber) {
- return 1;
- }
- // Lastly, sort by set number. I.e. Semi Final 1 Match 1 happens first.
- // Then comes Semi Final 2 Match 1. Then comes Semi Final 1 Match 2. Then
- // Semi Final 2 Match 2.
- const aSetNumber = a.setNumber();
- const bSetNumber = b.setNumber();
- if (aSetNumber < bSetNumber) {
- return -1;
- }
- if (aSetNumber > bSetNumber) {
- return 1;
- }
- return 0;
- });
-
- return matchList;
+ const u8Buffer = new Uint8Array(resBuffer);
+ // Cache the response.
+ await db.matchListData.put({id: 1, data: u8Buffer});
+ return this.parseMatchList(u8Buffer);
} else {
+ const cachedResult = await db.matchListData.where({id: 1}).toArray();
+ if (cachedResult && cachedResult.length == 1) {
+ const u8Buffer = cachedResult[0].data;
+ return this.parseMatchList(u8Buffer);
+ }
const resBuffer = await res.arrayBuffer();
const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
const errorMessage = parsedResponse.errorMessage();
throw `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
}
}
+ parseMatchList(u8Buffer: Uint8Array): Match[] {
+ const fbBuffer = new ByteBuffer(u8Buffer);
+ const parsedResponse =
+ RequestAllMatchesResponse.getRootAsRequestAllMatchesResponse(fbBuffer);
+ // Convert the flatbuffer list into an array. That's more useful.
+ const matchList = [];
+ for (let i = 0; i < parsedResponse.matchListLength(); i++) {
+ matchList.push(parsedResponse.matchList(i));
+ }
+ // Sort the list so it is in chronological order.
+ matchList.sort((a, b) => {
+ // First sort by match type. E.g. finals are last.
+ const aMatchTypeIndex = MATCH_TYPE_ORDERING.indexOf(a.compLevel());
+ const bMatchTypeIndex = MATCH_TYPE_ORDERING.indexOf(b.compLevel());
+ if (aMatchTypeIndex < bMatchTypeIndex) {
+ return -1;
+ }
+ if (aMatchTypeIndex > bMatchTypeIndex) {
+ return 1;
+ }
+ // Then sort by match number. E.g. in semi finals, all match 1 rounds
+ // are done first. Then come match 2 rounds. And then, if necessary,
+ // the match 3 rounds.
+ const aMatchNumber = a.matchNumber();
+ const bMatchNumber = b.matchNumber();
+ if (aMatchNumber < bMatchNumber) {
+ return -1;
+ }
+ if (aMatchNumber > bMatchNumber) {
+ return 1;
+ }
+ // Lastly, sort by set number. I.e. Semi Final 1 Match 1 happens first.
+ // Then comes Semi Final 2 Match 1. Then comes Semi Final 1 Match 2. Then
+ // Semi Final 2 Match 2.
+ const aSetNumber = a.setNumber();
+ const bSetNumber = b.setNumber();
+ if (aSetNumber < bSetNumber) {
+ return -1;
+ }
+ if (aSetNumber > bSetNumber) {
+ return 1;
+ }
+ return 0;
+ });
+ return matchList;
+ }
}
diff --git a/scouting/www/scan/BUILD b/scouting/www/scan/BUILD
new file mode 100644
index 0000000..88b2822
--- /dev/null
+++ b/scouting/www/scan/BUILD
@@ -0,0 +1,18 @@
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
+
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
+ name = "scan",
+ extra_srcs = [
+ "//scouting/www:app_common_css",
+ ],
+ deps = [
+ ":node_modules/@angular/forms",
+ ":node_modules/@types/pako",
+ ":node_modules/pako",
+ "//scouting/webserver/requests/messages:error_response_ts_fbs",
+ "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+ ],
+)
diff --git a/scouting/www/scan/package.json b/scouting/www/scan/package.json
new file mode 100644
index 0000000..a5950c3
--- /dev/null
+++ b/scouting/www/scan/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@org_frc971/scouting/www/scan",
+ "private": true,
+ "dependencies": {
+ "pako": "2.1.0",
+ "@types/pako": "2.0.3",
+ "@angular/forms": "v16-lts"
+ }
+}
diff --git a/scouting/www/scan/scan.component.css b/scouting/www/scan/scan.component.css
new file mode 100644
index 0000000..11cfe09
--- /dev/null
+++ b/scouting/www/scan/scan.component.css
@@ -0,0 +1,20 @@
+video {
+ width: 100%;
+ aspect-ratio: 1 / 1;
+}
+
+canvas {
+ /* We don't want to show the frames that we are scanning for QR codes. It's
+ * nicer to just see the video stream. */
+ display: none;
+}
+
+ul {
+ margin: 0px;
+}
+
+li > a.active {
+ /* Set the scanned QR codes to a green color. */
+ background-color: #198754;
+ border-color: #005700;
+}
diff --git a/scouting/www/scan/scan.component.ts b/scouting/www/scan/scan.component.ts
new file mode 100644
index 0000000..98a7b61
--- /dev/null
+++ b/scouting/www/scan/scan.component.ts
@@ -0,0 +1,236 @@
+import {Component, NgZone, OnInit, ViewChild, ElementRef} from '@angular/core';
+import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated';
+import {Builder, ByteBuffer} from 'flatbuffers';
+import * as pako from 'pako';
+
+declare var cv: any;
+declare var Module: any;
+
+// The number of milliseconds between QR code scans.
+const SCAN_PERIOD = 500;
+
+@Component({
+ selector: 'app-scan',
+ templateUrl: './scan.ng.html',
+ styleUrls: ['../app/common.css', './scan.component.css'],
+})
+export class ScanComponent implements OnInit {
+ @ViewChild('video')
+ public video: ElementRef;
+
+ @ViewChild('canvas')
+ public canvas: ElementRef;
+
+ errorMessage: string = '';
+ progressMessage: string = 'Waiting for QR code(s)';
+ scanComplete: boolean = false;
+ videoStartedSuccessfully = false;
+
+ qrCodeValuePieces: string[] = [];
+ qrCodeValuePieceSize = 0;
+
+ scanStream: MediaStream | null = null;
+ scanTimer: ReturnType<typeof setTimeout> | null = null;
+
+ constructor(private ngZone: NgZone) {}
+
+ ngOnInit() {
+ // If the user switched away from this tab, then the onRuntimeInitialized
+ // attribute will already be set. No need to load OpenCV again. If it's not
+ // loaded, however, we need to load it.
+ if (!Module['onRuntimeInitialized']) {
+ Module['onRuntimeInitialized'] = () => {
+ // Since the WASM code doesn't know about the Angular zone, we force
+ // it into the correct zone so that the UI gets updated properly.
+ this.ngZone.run(() => {
+ this.startScanning();
+ });
+ };
+ // Now that we set up the hook, we can load OpenCV.
+ this.loadOpenCv();
+ } else {
+ this.startScanning();
+ }
+ }
+
+ ngOnDestroy() {
+ clearInterval(this.scanTimer);
+
+ // Explicitly stop the streams so that the camera isn't being locked
+ // unnecessarily. I.e. other processes can use it too.
+ if (this.scanStream) {
+ this.scanStream.getTracks().forEach((track) => {
+ track.stop();
+ });
+ }
+ }
+
+ public ngAfterViewInit() {
+ // Start the video playback.
+ // It would be nice to let the user select which camera gets used. For now,
+ // we give the "environment" hint so that it faces away from the user.
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+ navigator.mediaDevices
+ .getUserMedia({video: {facingMode: 'environment'}})
+ .then(
+ (stream) => {
+ this.scanStream = stream;
+ this.video.nativeElement.srcObject = stream;
+ this.video.nativeElement.play();
+ this.videoStartedSuccessfully = true;
+ },
+ (reason) => {
+ this.progressMessage = '';
+ this.errorMessage = `Failed to start video: ${reason}`;
+ }
+ );
+ }
+ }
+
+ async scan() {
+ if (!this.videoStartedSuccessfully) {
+ return;
+ }
+
+ // Take a capture of the video stream. That capture can then be used by
+ // OpenCV to perform the QR code detection. Due to my inexperience, I could
+ // only make this code work if I size the (invisible) canvas to match the
+ // video element. Otherwise, I'd get cropped images.
+ // Can we stream the video directly into the canvas?
+ const width = this.video.nativeElement.clientWidth;
+ const height = this.video.nativeElement.clientHeight;
+ this.canvas.nativeElement.width = width;
+ this.canvas.nativeElement.height = height;
+ this.canvas.nativeElement
+ .getContext('2d')
+ .drawImage(this.video.nativeElement, 0, 0, width, height);
+
+ // Perform the QR code detection. We use the Aruco-variant of the detector
+ // here because it appears to detect QR codes much more reliably than the
+ // standard detector.
+ let mat = cv.imread('canvas');
+ let qrDecoder = new cv.QRCodeDetectorAruco();
+ const result = qrDecoder.detectAndDecode(mat);
+ mat.delete();
+
+ // Handle the result.
+ if (result) {
+ await this.scanSuccessHandler(result);
+ } else {
+ await this.scanFailureHandler();
+ }
+ }
+
+ async scanSuccessHandler(scanResult: string) {
+ // Reverse the conversion and obtain the original Uint8Array. In other
+ // words, undo the work in `scouting/www/entry/entry.component.ts`.
+ const [indexStr, numPiecesStr, pieceSizeStr, splitPiece] = scanResult.split(
+ '_',
+ 4
+ );
+
+ // If we didn't get enough data, then maybe we scanned some non-scouting
+ // related QR code? Try to give a hint to the user.
+ if (!indexStr || !numPiecesStr || !pieceSizeStr || !splitPiece) {
+ this.progressMessage = '';
+ this.errorMessage = `Couldn't find scouting data in the QR code.`;
+ return;
+ }
+
+ const index = Number(indexStr);
+ const numPieces = Number(numPiecesStr);
+ const pieceSize = Number(pieceSizeStr);
+
+ if (
+ numPieces != this.qrCodeValuePieces.length ||
+ pieceSize != this.qrCodeValuePieceSize
+ ) {
+ // The number of pieces or the piece size changed. We need to reset our accounting.
+ this.qrCodeValuePieces = new Array<string>(numPieces);
+ this.qrCodeValuePieceSize = pieceSize;
+ }
+
+ this.qrCodeValuePieces[index] = splitPiece;
+ this.progressMessage = `Scanned QR code ${index + 1} out of ${
+ this.qrCodeValuePieces.length
+ }`;
+
+ // Count up the number of missing pieces so we can give a progress update.
+ let numMissingPieces = 0;
+ for (const piece of this.qrCodeValuePieces) {
+ if (!piece) {
+ numMissingPieces++;
+ }
+ }
+ if (numMissingPieces > 0) {
+ this.progressMessage = `Waiting for ${numMissingPieces} out of ${this.qrCodeValuePieces.length} QR codes.`;
+ this.errorMessage = '';
+ return;
+ }
+
+ // Stop scanning now that we have all the pieces.
+ this.progressMessage = 'Scanned all QR codes. Submitting.';
+ this.scanComplete = true;
+ clearInterval(this.scanTimer);
+
+ const encodedData = this.qrCodeValuePieces.join('');
+ const deflatedData = Uint8Array.from(atob(encodedData), (c) =>
+ c.charCodeAt(0)
+ );
+ const actionBuffer = pako.inflate(deflatedData);
+
+ const res = await fetch('/requests/submit/submit_2024_actions', {
+ method: 'POST',
+ body: actionBuffer,
+ });
+
+ if (res.ok) {
+ // We successfully submitted the data. Report success.
+ this.progressMessage = 'Success!';
+ this.errorMessage = '';
+ } else {
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
+
+ const errorMessage = parsedResponse.errorMessage();
+ this.progressMessage = '';
+ this.errorMessage = `Submission failed with ${res.status} ${res.statusText}: "${errorMessage}"`;
+ }
+ }
+
+ async scanFailureHandler() {
+ this.progressMessage = '';
+ this.errorMessage = 'Failed to scan!';
+ }
+
+ loadOpenCv() {
+ // Make the browser load OpenCV.
+ let body = <HTMLDivElement>document.body;
+ let script = document.createElement('script');
+ script.innerHTML = '';
+ script.src = 'assets/opencv_4.9.0/opencv.js';
+ script.async = false;
+ script.defer = true;
+ script.onerror = (error) => {
+ this.progressMessage = '';
+ if (typeof error === 'string') {
+ this.errorMessage = `OpenCV failed to load: ${error}`;
+ } else {
+ this.errorMessage = 'OpenCV failed to load.';
+ }
+ // Since we use the onRuntimeInitialized property as a flag to see if we
+ // need to perform loading, we need to delete the property. When the user
+ // switches away from this tab and then switches back, we want to attempt
+ // loading again.
+ delete Module['onRuntimeInitialized'];
+ };
+ body.appendChild(script);
+ }
+
+ startScanning() {
+ this.scanTimer = setInterval(() => {
+ this.scan();
+ }, SCAN_PERIOD);
+ }
+}
diff --git a/scouting/www/scan/scan.module.ts b/scouting/www/scan/scan.module.ts
new file mode 100644
index 0000000..00a9c58
--- /dev/null
+++ b/scouting/www/scan/scan.module.ts
@@ -0,0 +1,10 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {ScanComponent} from './scan.component';
+
+@NgModule({
+ declarations: [ScanComponent],
+ exports: [ScanComponent],
+ imports: [CommonModule],
+})
+export class ScanModule {}
diff --git a/scouting/www/scan/scan.ng.html b/scouting/www/scan/scan.ng.html
new file mode 100644
index 0000000..f9b82b3
--- /dev/null
+++ b/scouting/www/scan/scan.ng.html
@@ -0,0 +1,21 @@
+<h1>Scan</h1>
+<span class="progress_message" role="alert">{{ progressMessage }}</span>
+<span class="error_message" role="alert">{{ errorMessage }}</span>
+<nav class="qrcode-progress" *ngIf="!scanComplete">
+ <ul class="pagination pagination-lg justify-content-center">
+ <li *ngFor="let piece of qrCodeValuePieces" class="page-item">
+ <a class="page-link" href="#" [class.active]="piece">
+ <i *ngIf="piece" class="bi bi-check">
+ <span class="visually-hidden">✓</span>
+ </i>
+ <i *ngIf="!piece" class="bi bi-camera">
+ <span class="visually-hidden">☒</span>
+ </i>
+ </a>
+ </li>
+ </ul>
+</nav>
+<div *ngIf="!scanComplete">
+ <video #video id="video"></video>
+</div>
+<canvas #canvas id="canvas"></canvas>
diff --git a/third_party/rules_cypress/0001-fix-incorrect-linux-checksums.patch b/third_party/rules_cypress/0001-fix-incorrect-linux-checksums.patch
new file mode 100644
index 0000000..b69e828
--- /dev/null
+++ b/third_party/rules_cypress/0001-fix-incorrect-linux-checksums.patch
@@ -0,0 +1,42 @@
+From 500a658a00cbd7839d2431e51003429d542b7f4a Mon Sep 17 00:00:00 2001
+From: Jack Vincent <50340752+jackvincentnz@users.noreply.github.com>
+Date: Fri, 19 Jan 2024 21:12:12 -0800
+Subject: [PATCH] fix: incorrect linux checksums
+
+---
+ cypress/private/versions.bzl | 12 ++++++------
+ 1 file changed, 6 insertions(+), 6 deletions(-)
+
+diff --git a/cypress/private/versions.bzl b/cypress/private/versions.bzl
+index 8fad4be..f4b3a1f 100644
+--- a/cypress/private/versions.bzl
++++ b/cypress/private/versions.bzl
+@@ -6,22 +6,22 @@ TOOL_VERSIONS = {
+ "13.3.1": {
+ "darwin-x64": "417c5f1d77e15c8aef0a55f155c16c3dbbc637f918c1e51f8fec6eb1c73a9ba9",
+ "darwin-arm64": "143d905779c0b0a8a9049b0eb68b4b156db3d838d4546ce5082a8f7bd5dc5232",
+- "linux-x64": "a165aa290d23d03f191852df2e8c3cb25ddccf1d4e226518a0a217f4ec405328",
+- "linux-arm64": "b51810d7fda748f67d5d875d5e265ee22bb394af2bb4a545b78c99f1aae91cb0",
++ "linux-x64": "bb0ddd980bd82792a477b1c39ce8a0a7b275481031c559c065c94f1131151b0c",
++ "linux-arm64": "fbca9958e2a153f3f1ffdef1bb506db65401a8586229b9d9424cd16228d0353d",
+ "win32-x64": "acf1e478634e4da254bd7c3695d9032010c2ed17955e7339e1ea7d79cf8c9f7b",
+ },
+ "12.12.0": {
+ "darwin-x64": "53ddd917112a2c5c3c22d12f5bcffda596474c7cd0932a997e575b2b97ae36c0",
+ "darwin-arm64": "2daadfe4431a98f9dc6442c155239aaed2746b12a59721424e3b5fdaaf27c766",
+- "linux-x64": "18bf251f683e0b0ca70918c2a51b7a457be6e5298be52203bd16d4e0eb361837",
+- "linux-arm64": "1f754c912eb719d4ac4abe31f3cc6d5b65cf08e0266cf6808f376c099928c88e",
++ "linux-x64": "7f41d45da094380cc0d6716c8357b60f9c9304c2475cf502ea60649f040d52ad",
++ "linux-arm64": "55531b5ba8d03a979a5ef92981d27964583897c652eec3788f24ec8677d05dd2",
+ "win32-x64": "ffc47314ce5f74888066bc4a15ee18d375ee9680f9cca1b94eda7293e1dea4e5",
+ },
+ "12.3.0": {
+ "darwin-x64": "beae3678dd859ce89cc45c377eef97b67558ee1f2a0079e9b4c824260ef3801a",
+ "darwin-arm64": "bb08f247110dda9b180d2552a661b8669441f931b0332d818c306a14e8c7071a",
+- "linux-x64": "57dd85936373e6ce2ae5378f9035a3ad118899341e0c6e71783c3e58c039ce92",
+- "linux-arm64": "47c1188506b11644a332ab0949eab0b33179a64e4857e561d3c836c6f6f2cadf",
++ "linux-x64": "3a300d6c903a8f5fced488183dcc7faa06e9df14c946d6dab4b5822ec738e9cd",
++ "linux-arm64": "501671011a63fd450b87e1cae1b3ba3fabccf37e9c1c8c26e1d5f189f9afe688",
+ "win32-x64": "639a0e0ca5498fc5330064c3fa441c741e6b6cd01234bfa9851de9a33f4f56a6",
+ },
+ "10.8.0": {
diff --git a/third_party/rules_cypress/0002-Add-support-for-cypress-13.6.6.patch b/third_party/rules_cypress/0002-Add-support-for-cypress-13.6.6.patch
new file mode 100644
index 0000000..d4d9144
--- /dev/null
+++ b/third_party/rules_cypress/0002-Add-support-for-cypress-13.6.6.patch
@@ -0,0 +1,27 @@
+From 2c8ec34c8fd4f6f485a7c7036e73fa82cd99859d Mon Sep 17 00:00:00 2001
+From: Philipp Schrader <philipp.schrader@gmail.com>
+Date: Sat, 24 Feb 2024 16:08:46 -0800
+Subject: [PATCH] Add support for cypress 13.6.6
+
+---
+ cypress/private/versions.bzl | 7 +++++++
+ 1 file changed, 7 insertions(+)
+
+diff --git a/cypress/private/versions.bzl b/cypress/private/versions.bzl
+index f4b3a1f..6a3e665 100644
+--- a/cypress/private/versions.bzl
++++ b/cypress/private/versions.bzl
+@@ -3,6 +3,13 @@
+ # Use /scripts/mirror_release.sh to add a newer version below.
+ # Versions should be descending order so TOOL_VERSIONS.keys()[0] is the latest version.
+ TOOL_VERSIONS = {
++ "13.6.6": {
++ "darwin-x64": "4b845921f35f520b8217a4ebb9106180c56de37a9db7f57f14352060c9eddca6",
++ "darwin-arm64": "4c8818534bdab028a82aefc1936a552481fa0efb2a7c5c64350e9d9d1c709c85",
++ "linux-x64": "0fe6f52c41912245a71f71c0978a3a64e0dcc9dec4449809c5bf0442032a9912",
++ "linux-arm64": "0fc709f4ae9121f5f7f1bb68423b8ec9c9d9f6c9baf85fcbe0cde3ee9627cc38",
++ "win32-x64": "a31751f41f74af9c3f74860c00d50d0a0e5d5a5fa7c9049db9d56c6082382483",
++ },
+ "13.3.1": {
+ "darwin-x64": "417c5f1d77e15c8aef0a55f155c16c3dbbc637f918c1e51f8fec6eb1c73a9ba9",
+ "darwin-arm64": "143d905779c0b0a8a9049b0eb68b4b156db3d838d4546ce5082a8f7bd5dc5232",
diff --git a/tools/build_rules/js.bzl b/tools/build_rules/js.bzl
index 51fe987..c2468f8 100644
--- a/tools/build_rules/js.bzl
+++ b/tools/build_rules/js.bzl
@@ -378,7 +378,7 @@
copy_file(
name = name + "_config",
- out = "cypress.config.js",
+ out = name + "_cypress.config.js",
src = "//tools/build_rules/js:cypress.config.js",
visibility = ["//visibility:private"],
)
@@ -392,7 +392,7 @@
name = name,
args = [
"run",
- "--config-file=cypress.config.js",
+ "--config-file=%s_cypress.config.js" % name,
"--browser=" + chrome_location,
],
browsers = ["@chrome_linux//:all"],
diff --git a/tools/build_rules/js/BUILD b/tools/build_rules/js/BUILD
index 1bbe769..86a0288 100644
--- a/tools/build_rules/js/BUILD
+++ b/tools/build_rules/js/BUILD
@@ -24,3 +24,11 @@
"//:node_modules/@types/node",
],
)
+
+py_binary(
+ name = "assemble_service_worker_files",
+ srcs = [
+ "assemble_service_worker_files.py",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/tools/build_rules/js/assemble_service_worker_files.py b/tools/build_rules/js/assemble_service_worker_files.py
new file mode 100644
index 0000000..d4485d5
--- /dev/null
+++ b/tools/build_rules/js/assemble_service_worker_files.py
@@ -0,0 +1,27 @@
+import argparse
+import shutil
+import sys
+from pathlib import Path
+
+
+def main(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--input_dir", type=Path, action="append", default=[])
+ parser.add_argument("--output", type=Path, action="append", default=[])
+ parser.add_argument("--relative_output",
+ type=Path,
+ action="append",
+ default=[])
+ args = parser.parse_args(argv[1:])
+
+ for relative_output, output in zip(args.relative_output, args.output):
+ for input_dir in args.input_dir:
+ input_file = input_dir / relative_output
+ if input_file.exists():
+ print(f"Copying {input_file} to {output}")
+ shutil.copy(input_file, output)
+ break
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/tools/build_rules/js/cypress.config.js b/tools/build_rules/js/cypress.config.js
index e991016..5d2ec1c 100644
--- a/tools/build_rules/js/cypress.config.js
+++ b/tools/build_rules/js/cypress.config.js
@@ -9,6 +9,14 @@
launchOptions.args.push('--disable-gpu-shader-disk-cache');
launchOptions.args.push('--enable-logging');
launchOptions.args.push('--v=stderr');
+
+ // Point the browser at a video file to use as a webcam. This lets us
+ // validate things like QR code scanning.
+ launchOptions.args.push('--use-fake-ui-for-media-stream');
+ launchOptions.args.push('--use-fake-device-for-media-stream');
+ const fakeCameraVideo = `${process.env.TEST_UNDECLARED_OUTPUTS_DIR}/fake_camera.mjpeg`;
+ launchOptions.args.push(`--use-file-for-fake-video-capture=${fakeCameraVideo}`);
+
return launchOptions;
});
diff --git a/tools/dependency_rewrite b/tools/dependency_rewrite
index 1cd117a..14980dd 100644
--- a/tools/dependency_rewrite
+++ b/tools/dependency_rewrite
@@ -18,6 +18,7 @@
rewrite cdn.cypress.io/(.*) software.frc971.org/Build-Dependencies/cdn.cypress.io/$1
rewrite www.googleapis.com/(.*) software.frc971.org/Build-Dependencies/www.googleapis.com/$1
rewrite www.johnvansickle.com/(.*) software.frc971.org/Build-Dependencies/www.johnvansickle.com/$1
+rewrite docs.opencv.org/(.*) software.frc971.org/Build-Dependencies/docs.opencv.org/$1
allow crates.io
allow golang.org
allow go.dev