Showing preview only (4,304K chars total). Download the full file or copy to clipboard to get everything.
Repository: Team254/cheesy-arena
Branch: main
Commit: 2102f67d1c56
Files: 1621
Total size: 3.9 MB
Directory structure:
gitextract_jrfey4xk/
├── .editorconfig
├── .github/
│ └── workflows/
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── AGENTS.md
├── LICENSE
├── README.md
├── coverage
├── field/
│ ├── arena.go
│ ├── arena_notifiers.go
│ ├── arena_test.go
│ ├── display.go
│ ├── display_test.go
│ ├── driver_station_connection.go
│ ├── driver_station_connection_test.go
│ ├── event_status.go
│ ├── event_status_test.go
│ ├── fake_plc_test.go
│ ├── realtime_score.go
│ ├── scoring_panel_registry.go
│ ├── scoring_panel_registry_test.go
│ ├── team_match_log.go
│ ├── team_sign.go
│ ├── team_sign_test.go
│ └── test_helpers.go
├── fix_avatar_colors_for_overlay
├── font/
│ ├── helvetica.json
│ ├── helveticab.json
│ ├── helveticabi.json
│ └── helveticai.json
├── game/
│ ├── foul.go
│ ├── match_sounds.go
│ ├── match_timing.go
│ ├── ranking_fields.go
│ ├── ranking_fields_test.go
│ ├── reef.go
│ ├── reef_test.go
│ ├── rule.go
│ ├── rule_test.go
│ ├── score.go
│ ├── score_summary.go
│ ├── score_summary_test.go
│ ├── score_test.go
│ └── test_helpers.go
├── go.mod
├── go.sum
├── main.go
├── model/
│ ├── alliance.go
│ ├── alliance_test.go
│ ├── award.go
│ ├── award_test.go
│ ├── database.go
│ ├── database_test.go
│ ├── event_settings.go
│ ├── event_settings_test.go
│ ├── judging_slot.go
│ ├── judging_slot_test.go
│ ├── lower_third.go
│ ├── lower_third_test.go
│ ├── match.go
│ ├── match_result.go
│ ├── match_result_test.go
│ ├── match_test.go
│ ├── matchtype_string.go
│ ├── ranking.go
│ ├── ranking_test.go
│ ├── schedule_block.go
│ ├── schedule_block_test.go
│ ├── scheduled_break.go
│ ├── scheduled_break_test.go
│ ├── sponsor_slide.go
│ ├── sponsor_slide_test.go
│ ├── table.go
│ ├── table_test.go
│ ├── team.go
│ ├── team_test.go
│ ├── test_helpers.go
│ ├── user_session.go
│ └── user_session_test.go
├── network/
│ ├── access_point.go
│ ├── access_point_test.go
│ ├── sccswitch.go
│ ├── sccswitch_test.go
│ ├── switch.go
│ ├── switch_test.go
│ └── testdata/
│ ├── iwinfo_0_teams.txt
│ ├── iwinfo_2_teams.txt
│ ├── iwinfo_6_teams.txt
│ └── iwinfo_invalid.txt
├── partner/
│ ├── blackmagic.go
│ ├── blackmagic_test.go
│ ├── companion.go
│ ├── companion_test.go
│ ├── nexus.go
│ ├── nexus_test.go
│ ├── tba.go
│ └── tba_test.go
├── playoff/
│ ├── alliance_source.go
│ ├── break_spec.go
│ ├── double_elimination.go
│ ├── double_elimination_test.go
│ ├── match_group.go
│ ├── match_group_test.go
│ ├── matchup.go
│ ├── matchup_test.go
│ ├── playoff_match_result.go
│ ├── playoff_tournament.go
│ ├── playoff_tournament_test.go
│ ├── single_elimination.go
│ ├── single_elimination_test.go
│ └── test_helpers.go
├── plc/
│ ├── armorblock_string.go
│ ├── coil_string.go
│ ├── fake_modbus_client_test.go
│ ├── input_string.go
│ ├── plc.go
│ ├── plc_test.go
│ └── register_string.go
├── schedules/
│ ├── 100_1.csv
│ ├── 100_10.csv
│ ├── 100_11.csv
│ ├── 100_12.csv
│ ├── 100_13.csv
│ ├── 100_14.csv
│ ├── 100_2.csv
│ ├── 100_3.csv
│ ├── 100_4.csv
│ ├── 100_5.csv
│ ├── 100_6.csv
│ ├── 100_7.csv
│ ├── 100_8.csv
│ ├── 100_9.csv
│ ├── 10_1.csv
│ ├── 10_10.csv
│ ├── 10_11.csv
│ ├── 10_12.csv
│ ├── 10_13.csv
│ ├── 10_14.csv
│ ├── 10_2.csv
│ ├── 10_3.csv
│ ├── 10_4.csv
│ ├── 10_5.csv
│ ├── 10_6.csv
│ ├── 10_7.csv
│ ├── 10_8.csv
│ ├── 10_9.csv
│ ├── 11_1.csv
│ ├── 11_10.csv
│ ├── 11_11.csv
│ ├── 11_12.csv
│ ├── 11_13.csv
│ ├── 11_14.csv
│ ├── 11_2.csv
│ ├── 11_3.csv
│ ├── 11_4.csv
│ ├── 11_5.csv
│ ├── 11_6.csv
│ ├── 11_7.csv
│ ├── 11_8.csv
│ ├── 11_9.csv
│ ├── 12_1.csv
│ ├── 12_10.csv
│ ├── 12_11.csv
│ ├── 12_12.csv
│ ├── 12_13.csv
│ ├── 12_14.csv
│ ├── 12_2.csv
│ ├── 12_3.csv
│ ├── 12_4.csv
│ ├── 12_5.csv
│ ├── 12_6.csv
│ ├── 12_7.csv
│ ├── 12_8.csv
│ ├── 12_9.csv
│ ├── 13_1.csv
│ ├── 13_10.csv
│ ├── 13_11.csv
│ ├── 13_12.csv
│ ├── 13_13.csv
│ ├── 13_14.csv
│ ├── 13_2.csv
│ ├── 13_3.csv
│ ├── 13_4.csv
│ ├── 13_5.csv
│ ├── 13_6.csv
│ ├── 13_7.csv
│ ├── 13_8.csv
│ ├── 13_9.csv
│ ├── 14_1.csv
│ ├── 14_10.csv
│ ├── 14_11.csv
│ ├── 14_12.csv
│ ├── 14_13.csv
│ ├── 14_14.csv
│ ├── 14_2.csv
│ ├── 14_3.csv
│ ├── 14_4.csv
│ ├── 14_5.csv
│ ├── 14_6.csv
│ ├── 14_7.csv
│ ├── 14_8.csv
│ ├── 14_9.csv
│ ├── 15_1.csv
│ ├── 15_10.csv
│ ├── 15_11.csv
│ ├── 15_12.csv
│ ├── 15_13.csv
│ ├── 15_14.csv
│ ├── 15_2.csv
│ ├── 15_3.csv
│ ├── 15_4.csv
│ ├── 15_5.csv
│ ├── 15_6.csv
│ ├── 15_7.csv
│ ├── 15_8.csv
│ ├── 15_9.csv
│ ├── 16_1.csv
│ ├── 16_10.csv
│ ├── 16_11.csv
│ ├── 16_12.csv
│ ├── 16_13.csv
│ ├── 16_14.csv
│ ├── 16_2.csv
│ ├── 16_3.csv
│ ├── 16_4.csv
│ ├── 16_5.csv
│ ├── 16_6.csv
│ ├── 16_7.csv
│ ├── 16_8.csv
│ ├── 16_9.csv
│ ├── 17_1.csv
│ ├── 17_10.csv
│ ├── 17_11.csv
│ ├── 17_12.csv
│ ├── 17_13.csv
│ ├── 17_14.csv
│ ├── 17_2.csv
│ ├── 17_3.csv
│ ├── 17_4.csv
│ ├── 17_5.csv
│ ├── 17_6.csv
│ ├── 17_7.csv
│ ├── 17_8.csv
│ ├── 17_9.csv
│ ├── 18_1.csv
│ ├── 18_10.csv
│ ├── 18_11.csv
│ ├── 18_12.csv
│ ├── 18_13.csv
│ ├── 18_14.csv
│ ├── 18_2.csv
│ ├── 18_3.csv
│ ├── 18_4.csv
│ ├── 18_5.csv
│ ├── 18_6.csv
│ ├── 18_7.csv
│ ├── 18_8.csv
│ ├── 18_9.csv
│ ├── 19_1.csv
│ ├── 19_10.csv
│ ├── 19_11.csv
│ ├── 19_12.csv
│ ├── 19_13.csv
│ ├── 19_14.csv
│ ├── 19_2.csv
│ ├── 19_3.csv
│ ├── 19_4.csv
│ ├── 19_5.csv
│ ├── 19_6.csv
│ ├── 19_7.csv
│ ├── 19_8.csv
│ ├── 19_9.csv
│ ├── 20_1.csv
│ ├── 20_10.csv
│ ├── 20_11.csv
│ ├── 20_12.csv
│ ├── 20_13.csv
│ ├── 20_14.csv
│ ├── 20_2.csv
│ ├── 20_3.csv
│ ├── 20_4.csv
│ ├── 20_5.csv
│ ├── 20_6.csv
│ ├── 20_7.csv
│ ├── 20_8.csv
│ ├── 20_9.csv
│ ├── 21_1.csv
│ ├── 21_10.csv
│ ├── 21_11.csv
│ ├── 21_12.csv
│ ├── 21_13.csv
│ ├── 21_14.csv
│ ├── 21_2.csv
│ ├── 21_3.csv
│ ├── 21_4.csv
│ ├── 21_5.csv
│ ├── 21_6.csv
│ ├── 21_7.csv
│ ├── 21_8.csv
│ ├── 21_9.csv
│ ├── 22_1.csv
│ ├── 22_10.csv
│ ├── 22_11.csv
│ ├── 22_12.csv
│ ├── 22_13.csv
│ ├── 22_14.csv
│ ├── 22_2.csv
│ ├── 22_3.csv
│ ├── 22_4.csv
│ ├── 22_5.csv
│ ├── 22_6.csv
│ ├── 22_7.csv
│ ├── 22_8.csv
│ ├── 22_9.csv
│ ├── 23_1.csv
│ ├── 23_10.csv
│ ├── 23_11.csv
│ ├── 23_12.csv
│ ├── 23_13.csv
│ ├── 23_14.csv
│ ├── 23_2.csv
│ ├── 23_3.csv
│ ├── 23_4.csv
│ ├── 23_5.csv
│ ├── 23_6.csv
│ ├── 23_7.csv
│ ├── 23_8.csv
│ ├── 23_9.csv
│ ├── 24_1.csv
│ ├── 24_10.csv
│ ├── 24_11.csv
│ ├── 24_12.csv
│ ├── 24_13.csv
│ ├── 24_14.csv
│ ├── 24_2.csv
│ ├── 24_3.csv
│ ├── 24_4.csv
│ ├── 24_5.csv
│ ├── 24_6.csv
│ ├── 24_7.csv
│ ├── 24_8.csv
│ ├── 24_9.csv
│ ├── 25_1.csv
│ ├── 25_10.csv
│ ├── 25_11.csv
│ ├── 25_12.csv
│ ├── 25_13.csv
│ ├── 25_14.csv
│ ├── 25_2.csv
│ ├── 25_3.csv
│ ├── 25_4.csv
│ ├── 25_5.csv
│ ├── 25_6.csv
│ ├── 25_7.csv
│ ├── 25_8.csv
│ ├── 25_9.csv
│ ├── 26_1.csv
│ ├── 26_10.csv
│ ├── 26_11.csv
│ ├── 26_12.csv
│ ├── 26_13.csv
│ ├── 26_14.csv
│ ├── 26_2.csv
│ ├── 26_3.csv
│ ├── 26_4.csv
│ ├── 26_5.csv
│ ├── 26_6.csv
│ ├── 26_7.csv
│ ├── 26_8.csv
│ ├── 26_9.csv
│ ├── 27_1.csv
│ ├── 27_10.csv
│ ├── 27_11.csv
│ ├── 27_12.csv
│ ├── 27_13.csv
│ ├── 27_14.csv
│ ├── 27_2.csv
│ ├── 27_3.csv
│ ├── 27_4.csv
│ ├── 27_5.csv
│ ├── 27_6.csv
│ ├── 27_7.csv
│ ├── 27_8.csv
│ ├── 27_9.csv
│ ├── 28_1.csv
│ ├── 28_10.csv
│ ├── 28_11.csv
│ ├── 28_12.csv
│ ├── 28_13.csv
│ ├── 28_14.csv
│ ├── 28_2.csv
│ ├── 28_3.csv
│ ├── 28_4.csv
│ ├── 28_5.csv
│ ├── 28_6.csv
│ ├── 28_7.csv
│ ├── 28_8.csv
│ ├── 28_9.csv
│ ├── 29_1.csv
│ ├── 29_10.csv
│ ├── 29_11.csv
│ ├── 29_12.csv
│ ├── 29_13.csv
│ ├── 29_14.csv
│ ├── 29_2.csv
│ ├── 29_3.csv
│ ├── 29_4.csv
│ ├── 29_5.csv
│ ├── 29_6.csv
│ ├── 29_7.csv
│ ├── 29_8.csv
│ ├── 29_9.csv
│ ├── 30_1.csv
│ ├── 30_10.csv
│ ├── 30_11.csv
│ ├── 30_12.csv
│ ├── 30_13.csv
│ ├── 30_14.csv
│ ├── 30_2.csv
│ ├── 30_3.csv
│ ├── 30_4.csv
│ ├── 30_5.csv
│ ├── 30_6.csv
│ ├── 30_7.csv
│ ├── 30_8.csv
│ ├── 30_9.csv
│ ├── 31_1.csv
│ ├── 31_10.csv
│ ├── 31_11.csv
│ ├── 31_12.csv
│ ├── 31_13.csv
│ ├── 31_14.csv
│ ├── 31_2.csv
│ ├── 31_3.csv
│ ├── 31_4.csv
│ ├── 31_5.csv
│ ├── 31_6.csv
│ ├── 31_7.csv
│ ├── 31_8.csv
│ ├── 31_9.csv
│ ├── 32_1.csv
│ ├── 32_10.csv
│ ├── 32_11.csv
│ ├── 32_12.csv
│ ├── 32_13.csv
│ ├── 32_14.csv
│ ├── 32_2.csv
│ ├── 32_3.csv
│ ├── 32_4.csv
│ ├── 32_5.csv
│ ├── 32_6.csv
│ ├── 32_7.csv
│ ├── 32_8.csv
│ ├── 32_9.csv
│ ├── 33_1.csv
│ ├── 33_10.csv
│ ├── 33_11.csv
│ ├── 33_12.csv
│ ├── 33_13.csv
│ ├── 33_14.csv
│ ├── 33_2.csv
│ ├── 33_3.csv
│ ├── 33_4.csv
│ ├── 33_5.csv
│ ├── 33_6.csv
│ ├── 33_7.csv
│ ├── 33_8.csv
│ ├── 33_9.csv
│ ├── 34_1.csv
│ ├── 34_10.csv
│ ├── 34_11.csv
│ ├── 34_12.csv
│ ├── 34_13.csv
│ ├── 34_14.csv
│ ├── 34_2.csv
│ ├── 34_3.csv
│ ├── 34_4.csv
│ ├── 34_5.csv
│ ├── 34_6.csv
│ ├── 34_7.csv
│ ├── 34_8.csv
│ ├── 34_9.csv
│ ├── 35_1.csv
│ ├── 35_10.csv
│ ├── 35_11.csv
│ ├── 35_12.csv
│ ├── 35_13.csv
│ ├── 35_14.csv
│ ├── 35_2.csv
│ ├── 35_3.csv
│ ├── 35_4.csv
│ ├── 35_5.csv
│ ├── 35_6.csv
│ ├── 35_7.csv
│ ├── 35_8.csv
│ ├── 35_9.csv
│ ├── 36_1.csv
│ ├── 36_10.csv
│ ├── 36_11.csv
│ ├── 36_12.csv
│ ├── 36_13.csv
│ ├── 36_14.csv
│ ├── 36_2.csv
│ ├── 36_3.csv
│ ├── 36_4.csv
│ ├── 36_5.csv
│ ├── 36_6.csv
│ ├── 36_7.csv
│ ├── 36_8.csv
│ ├── 36_9.csv
│ ├── 37_1.csv
│ ├── 37_10.csv
│ ├── 37_11.csv
│ ├── 37_12.csv
│ ├── 37_13.csv
│ ├── 37_14.csv
│ ├── 37_2.csv
│ ├── 37_3.csv
│ ├── 37_4.csv
│ ├── 37_5.csv
│ ├── 37_6.csv
│ ├── 37_7.csv
│ ├── 37_8.csv
│ ├── 37_9.csv
│ ├── 38_1.csv
│ ├── 38_10.csv
│ ├── 38_11.csv
│ ├── 38_12.csv
│ ├── 38_13.csv
│ ├── 38_14.csv
│ ├── 38_2.csv
│ ├── 38_3.csv
│ ├── 38_4.csv
│ ├── 38_5.csv
│ ├── 38_6.csv
│ ├── 38_7.csv
│ ├── 38_8.csv
│ ├── 38_9.csv
│ ├── 39_1.csv
│ ├── 39_10.csv
│ ├── 39_11.csv
│ ├── 39_12.csv
│ ├── 39_13.csv
│ ├── 39_14.csv
│ ├── 39_2.csv
│ ├── 39_3.csv
│ ├── 39_4.csv
│ ├── 39_5.csv
│ ├── 39_6.csv
│ ├── 39_7.csv
│ ├── 39_8.csv
│ ├── 39_9.csv
│ ├── 40_1.csv
│ ├── 40_10.csv
│ ├── 40_11.csv
│ ├── 40_12.csv
│ ├── 40_13.csv
│ ├── 40_14.csv
│ ├── 40_2.csv
│ ├── 40_3.csv
│ ├── 40_4.csv
│ ├── 40_5.csv
│ ├── 40_6.csv
│ ├── 40_7.csv
│ ├── 40_8.csv
│ ├── 40_9.csv
│ ├── 41_1.csv
│ ├── 41_10.csv
│ ├── 41_11.csv
│ ├── 41_12.csv
│ ├── 41_13.csv
│ ├── 41_14.csv
│ ├── 41_2.csv
│ ├── 41_3.csv
│ ├── 41_4.csv
│ ├── 41_5.csv
│ ├── 41_6.csv
│ ├── 41_7.csv
│ ├── 41_8.csv
│ ├── 41_9.csv
│ ├── 42_1.csv
│ ├── 42_10.csv
│ ├── 42_11.csv
│ ├── 42_12.csv
│ ├── 42_13.csv
│ ├── 42_14.csv
│ ├── 42_2.csv
│ ├── 42_3.csv
│ ├── 42_4.csv
│ ├── 42_5.csv
│ ├── 42_6.csv
│ ├── 42_7.csv
│ ├── 42_8.csv
│ ├── 42_9.csv
│ ├── 43_1.csv
│ ├── 43_10.csv
│ ├── 43_11.csv
│ ├── 43_12.csv
│ ├── 43_13.csv
│ ├── 43_14.csv
│ ├── 43_2.csv
│ ├── 43_3.csv
│ ├── 43_4.csv
│ ├── 43_5.csv
│ ├── 43_6.csv
│ ├── 43_7.csv
│ ├── 43_8.csv
│ ├── 43_9.csv
│ ├── 44_1.csv
│ ├── 44_10.csv
│ ├── 44_11.csv
│ ├── 44_12.csv
│ ├── 44_13.csv
│ ├── 44_14.csv
│ ├── 44_2.csv
│ ├── 44_3.csv
│ ├── 44_4.csv
│ ├── 44_5.csv
│ ├── 44_6.csv
│ ├── 44_7.csv
│ ├── 44_8.csv
│ ├── 44_9.csv
│ ├── 45_1.csv
│ ├── 45_10.csv
│ ├── 45_11.csv
│ ├── 45_12.csv
│ ├── 45_13.csv
│ ├── 45_14.csv
│ ├── 45_2.csv
│ ├── 45_3.csv
│ ├── 45_4.csv
│ ├── 45_5.csv
│ ├── 45_6.csv
│ ├── 45_7.csv
│ ├── 45_8.csv
│ ├── 45_9.csv
│ ├── 46_1.csv
│ ├── 46_10.csv
│ ├── 46_11.csv
│ ├── 46_12.csv
│ ├── 46_13.csv
│ ├── 46_14.csv
│ ├── 46_2.csv
│ ├── 46_3.csv
│ ├── 46_4.csv
│ ├── 46_5.csv
│ ├── 46_6.csv
│ ├── 46_7.csv
│ ├── 46_8.csv
│ ├── 46_9.csv
│ ├── 47_1.csv
│ ├── 47_10.csv
│ ├── 47_11.csv
│ ├── 47_12.csv
│ ├── 47_13.csv
│ ├── 47_14.csv
│ ├── 47_2.csv
│ ├── 47_3.csv
│ ├── 47_4.csv
│ ├── 47_5.csv
│ ├── 47_6.csv
│ ├── 47_7.csv
│ ├── 47_8.csv
│ ├── 47_9.csv
│ ├── 48_1.csv
│ ├── 48_10.csv
│ ├── 48_11.csv
│ ├── 48_12.csv
│ ├── 48_13.csv
│ ├── 48_14.csv
│ ├── 48_2.csv
│ ├── 48_3.csv
│ ├── 48_4.csv
│ ├── 48_5.csv
│ ├── 48_6.csv
│ ├── 48_7.csv
│ ├── 48_8.csv
│ ├── 48_9.csv
│ ├── 49_1.csv
│ ├── 49_10.csv
│ ├── 49_11.csv
│ ├── 49_12.csv
│ ├── 49_13.csv
│ ├── 49_14.csv
│ ├── 49_2.csv
│ ├── 49_3.csv
│ ├── 49_4.csv
│ ├── 49_5.csv
│ ├── 49_6.csv
│ ├── 49_7.csv
│ ├── 49_8.csv
│ ├── 49_9.csv
│ ├── 50_1.csv
│ ├── 50_10.csv
│ ├── 50_11.csv
│ ├── 50_12.csv
│ ├── 50_13.csv
│ ├── 50_14.csv
│ ├── 50_2.csv
│ ├── 50_3.csv
│ ├── 50_4.csv
│ ├── 50_5.csv
│ ├── 50_6.csv
│ ├── 50_7.csv
│ ├── 50_8.csv
│ ├── 50_9.csv
│ ├── 51_1.csv
│ ├── 51_10.csv
│ ├── 51_11.csv
│ ├── 51_12.csv
│ ├── 51_13.csv
│ ├── 51_14.csv
│ ├── 51_2.csv
│ ├── 51_3.csv
│ ├── 51_4.csv
│ ├── 51_5.csv
│ ├── 51_6.csv
│ ├── 51_7.csv
│ ├── 51_8.csv
│ ├── 51_9.csv
│ ├── 52_1.csv
│ ├── 52_10.csv
│ ├── 52_11.csv
│ ├── 52_12.csv
│ ├── 52_13.csv
│ ├── 52_14.csv
│ ├── 52_2.csv
│ ├── 52_3.csv
│ ├── 52_4.csv
│ ├── 52_5.csv
│ ├── 52_6.csv
│ ├── 52_7.csv
│ ├── 52_8.csv
│ ├── 52_9.csv
│ ├── 53_1.csv
│ ├── 53_10.csv
│ ├── 53_11.csv
│ ├── 53_12.csv
│ ├── 53_13.csv
│ ├── 53_14.csv
│ ├── 53_2.csv
│ ├── 53_3.csv
│ ├── 53_4.csv
│ ├── 53_5.csv
│ ├── 53_6.csv
│ ├── 53_7.csv
│ ├── 53_8.csv
│ ├── 53_9.csv
│ ├── 54_1.csv
│ ├── 54_10.csv
│ ├── 54_11.csv
│ ├── 54_12.csv
│ ├── 54_13.csv
│ ├── 54_14.csv
│ ├── 54_2.csv
│ ├── 54_3.csv
│ ├── 54_4.csv
│ ├── 54_5.csv
│ ├── 54_6.csv
│ ├── 54_7.csv
│ ├── 54_8.csv
│ ├── 54_9.csv
│ ├── 55_1.csv
│ ├── 55_10.csv
│ ├── 55_11.csv
│ ├── 55_12.csv
│ ├── 55_13.csv
│ ├── 55_14.csv
│ ├── 55_2.csv
│ ├── 55_3.csv
│ ├── 55_4.csv
│ ├── 55_5.csv
│ ├── 55_6.csv
│ ├── 55_7.csv
│ ├── 55_8.csv
│ ├── 55_9.csv
│ ├── 56_1.csv
│ ├── 56_10.csv
│ ├── 56_11.csv
│ ├── 56_12.csv
│ ├── 56_13.csv
│ ├── 56_14.csv
│ ├── 56_2.csv
│ ├── 56_3.csv
│ ├── 56_4.csv
│ ├── 56_5.csv
│ ├── 56_6.csv
│ ├── 56_7.csv
│ ├── 56_8.csv
│ ├── 56_9.csv
│ ├── 57_1.csv
│ ├── 57_10.csv
│ ├── 57_11.csv
│ ├── 57_12.csv
│ ├── 57_13.csv
│ ├── 57_14.csv
│ ├── 57_2.csv
│ ├── 57_3.csv
│ ├── 57_4.csv
│ ├── 57_5.csv
│ ├── 57_6.csv
│ ├── 57_7.csv
│ ├── 57_8.csv
│ ├── 57_9.csv
│ ├── 58_1.csv
│ ├── 58_10.csv
│ ├── 58_11.csv
│ ├── 58_12.csv
│ ├── 58_13.csv
│ ├── 58_14.csv
│ ├── 58_2.csv
│ ├── 58_3.csv
│ ├── 58_4.csv
│ ├── 58_5.csv
│ ├── 58_6.csv
│ ├── 58_7.csv
│ ├── 58_8.csv
│ ├── 58_9.csv
│ ├── 59_1.csv
│ ├── 59_10.csv
│ ├── 59_11.csv
│ ├── 59_12.csv
│ ├── 59_13.csv
│ ├── 59_14.csv
│ ├── 59_2.csv
│ ├── 59_3.csv
│ ├── 59_4.csv
│ ├── 59_5.csv
│ ├── 59_6.csv
│ ├── 59_7.csv
│ ├── 59_8.csv
│ ├── 59_9.csv
│ ├── 60_1.csv
│ ├── 60_10.csv
│ ├── 60_11.csv
│ ├── 60_12.csv
│ ├── 60_13.csv
│ ├── 60_14.csv
│ ├── 60_2.csv
│ ├── 60_3.csv
│ ├── 60_4.csv
│ ├── 60_5.csv
│ ├── 60_6.csv
│ ├── 60_7.csv
│ ├── 60_8.csv
│ ├── 60_9.csv
│ ├── 61_1.csv
│ ├── 61_10.csv
│ ├── 61_11.csv
│ ├── 61_12.csv
│ ├── 61_13.csv
│ ├── 61_14.csv
│ ├── 61_2.csv
│ ├── 61_3.csv
│ ├── 61_4.csv
│ ├── 61_5.csv
│ ├── 61_6.csv
│ ├── 61_7.csv
│ ├── 61_8.csv
│ ├── 61_9.csv
│ ├── 62_1.csv
│ ├── 62_10.csv
│ ├── 62_11.csv
│ ├── 62_12.csv
│ ├── 62_13.csv
│ ├── 62_14.csv
│ ├── 62_2.csv
│ ├── 62_3.csv
│ ├── 62_4.csv
│ ├── 62_5.csv
│ ├── 62_6.csv
│ ├── 62_7.csv
│ ├── 62_8.csv
│ ├── 62_9.csv
│ ├── 63_1.csv
│ ├── 63_10.csv
│ ├── 63_11.csv
│ ├── 63_12.csv
│ ├── 63_13.csv
│ ├── 63_14.csv
│ ├── 63_2.csv
│ ├── 63_3.csv
│ ├── 63_4.csv
│ ├── 63_5.csv
│ ├── 63_6.csv
│ ├── 63_7.csv
│ ├── 63_8.csv
│ ├── 63_9.csv
│ ├── 64_1.csv
│ ├── 64_10.csv
│ ├── 64_11.csv
│ ├── 64_12.csv
│ ├── 64_13.csv
│ ├── 64_14.csv
│ ├── 64_2.csv
│ ├── 64_3.csv
│ ├── 64_4.csv
│ ├── 64_5.csv
│ ├── 64_6.csv
│ ├── 64_7.csv
│ ├── 64_8.csv
│ ├── 64_9.csv
│ ├── 65_1.csv
│ ├── 65_10.csv
│ ├── 65_11.csv
│ ├── 65_12.csv
│ ├── 65_13.csv
│ ├── 65_14.csv
│ ├── 65_2.csv
│ ├── 65_3.csv
│ ├── 65_4.csv
│ ├── 65_5.csv
│ ├── 65_6.csv
│ ├── 65_7.csv
│ ├── 65_8.csv
│ ├── 65_9.csv
│ ├── 66_1.csv
│ ├── 66_10.csv
│ ├── 66_11.csv
│ ├── 66_12.csv
│ ├── 66_13.csv
│ ├── 66_14.csv
│ ├── 66_2.csv
│ ├── 66_3.csv
│ ├── 66_4.csv
│ ├── 66_5.csv
│ ├── 66_6.csv
│ ├── 66_7.csv
│ ├── 66_8.csv
│ ├── 66_9.csv
│ ├── 67_1.csv
│ ├── 67_10.csv
│ ├── 67_11.csv
│ ├── 67_12.csv
│ ├── 67_13.csv
│ ├── 67_14.csv
│ ├── 67_2.csv
│ ├── 67_3.csv
│ ├── 67_4.csv
│ ├── 67_5.csv
│ ├── 67_6.csv
│ ├── 67_7.csv
│ ├── 67_8.csv
│ ├── 67_9.csv
│ ├── 68_1.csv
│ ├── 68_10.csv
│ ├── 68_11.csv
│ ├── 68_12.csv
│ ├── 68_13.csv
│ ├── 68_14.csv
│ ├── 68_2.csv
│ ├── 68_3.csv
│ ├── 68_4.csv
│ ├── 68_5.csv
│ ├── 68_6.csv
│ ├── 68_7.csv
│ ├── 68_8.csv
│ ├── 68_9.csv
│ ├── 69_1.csv
│ ├── 69_10.csv
│ ├── 69_11.csv
│ ├── 69_12.csv
│ ├── 69_13.csv
│ ├── 69_14.csv
│ ├── 69_2.csv
│ ├── 69_3.csv
│ ├── 69_4.csv
│ ├── 69_5.csv
│ ├── 69_6.csv
│ ├── 69_7.csv
│ ├── 69_8.csv
│ ├── 69_9.csv
│ ├── 6_1.csv
│ ├── 6_10.csv
│ ├── 6_11.csv
│ ├── 6_12.csv
│ ├── 6_13.csv
│ ├── 6_14.csv
│ ├── 6_2.csv
│ ├── 6_3.csv
│ ├── 6_4.csv
│ ├── 6_5.csv
│ ├── 6_6.csv
│ ├── 6_7.csv
│ ├── 6_8.csv
│ ├── 6_9.csv
│ ├── 70_1.csv
│ ├── 70_10.csv
│ ├── 70_11.csv
│ ├── 70_12.csv
│ ├── 70_13.csv
│ ├── 70_14.csv
│ ├── 70_2.csv
│ ├── 70_3.csv
│ ├── 70_4.csv
│ ├── 70_5.csv
│ ├── 70_6.csv
│ ├── 70_7.csv
│ ├── 70_8.csv
│ ├── 70_9.csv
│ ├── 71_1.csv
│ ├── 71_10.csv
│ ├── 71_11.csv
│ ├── 71_12.csv
│ ├── 71_13.csv
│ ├── 71_14.csv
│ ├── 71_2.csv
│ ├── 71_3.csv
│ ├── 71_4.csv
│ ├── 71_5.csv
│ ├── 71_6.csv
│ ├── 71_7.csv
│ ├── 71_8.csv
│ ├── 71_9.csv
│ ├── 72_1.csv
│ ├── 72_10.csv
│ ├── 72_11.csv
│ ├── 72_12.csv
│ ├── 72_13.csv
│ ├── 72_14.csv
│ ├── 72_2.csv
│ ├── 72_3.csv
│ ├── 72_4.csv
│ ├── 72_5.csv
│ ├── 72_6.csv
│ ├── 72_7.csv
│ ├── 72_8.csv
│ ├── 72_9.csv
│ ├── 73_1.csv
│ ├── 73_10.csv
│ ├── 73_11.csv
│ ├── 73_12.csv
│ ├── 73_13.csv
│ ├── 73_14.csv
│ ├── 73_2.csv
│ ├── 73_3.csv
│ ├── 73_4.csv
│ ├── 73_5.csv
│ ├── 73_6.csv
│ ├── 73_7.csv
│ ├── 73_8.csv
│ ├── 73_9.csv
│ ├── 74_1.csv
│ ├── 74_10.csv
│ ├── 74_11.csv
│ ├── 74_12.csv
│ ├── 74_13.csv
│ ├── 74_14.csv
│ ├── 74_2.csv
│ ├── 74_3.csv
│ ├── 74_4.csv
│ ├── 74_5.csv
│ ├── 74_6.csv
│ ├── 74_7.csv
│ ├── 74_8.csv
│ ├── 74_9.csv
│ ├── 75_1.csv
│ ├── 75_10.csv
│ ├── 75_11.csv
│ ├── 75_12.csv
│ ├── 75_13.csv
│ ├── 75_14.csv
│ ├── 75_2.csv
│ ├── 75_3.csv
│ ├── 75_4.csv
│ ├── 75_5.csv
│ ├── 75_6.csv
│ ├── 75_7.csv
│ ├── 75_8.csv
│ ├── 75_9.csv
│ ├── 76_1.csv
│ ├── 76_10.csv
│ ├── 76_11.csv
│ ├── 76_12.csv
│ ├── 76_13.csv
│ ├── 76_14.csv
│ ├── 76_2.csv
│ ├── 76_3.csv
│ ├── 76_4.csv
│ ├── 76_5.csv
│ ├── 76_6.csv
│ ├── 76_7.csv
│ ├── 76_8.csv
│ ├── 76_9.csv
│ ├── 77_1.csv
│ ├── 77_10.csv
│ ├── 77_11.csv
│ ├── 77_12.csv
│ ├── 77_13.csv
│ ├── 77_14.csv
│ ├── 77_2.csv
│ ├── 77_3.csv
│ ├── 77_4.csv
│ ├── 77_5.csv
│ ├── 77_6.csv
│ ├── 77_7.csv
│ ├── 77_8.csv
│ ├── 77_9.csv
│ ├── 78_1.csv
│ ├── 78_10.csv
│ ├── 78_11.csv
│ ├── 78_12.csv
│ ├── 78_13.csv
│ ├── 78_14.csv
│ ├── 78_2.csv
│ ├── 78_3.csv
│ ├── 78_4.csv
│ ├── 78_5.csv
│ ├── 78_6.csv
│ ├── 78_7.csv
│ ├── 78_8.csv
│ ├── 78_9.csv
│ ├── 79_1.csv
│ ├── 79_10.csv
│ ├── 79_11.csv
│ ├── 79_12.csv
│ ├── 79_13.csv
│ ├── 79_14.csv
│ ├── 79_2.csv
│ ├── 79_3.csv
│ ├── 79_4.csv
│ ├── 79_5.csv
│ ├── 79_6.csv
│ ├── 79_7.csv
│ ├── 79_8.csv
│ ├── 79_9.csv
│ ├── 7_1.csv
│ ├── 7_10.csv
│ ├── 7_11.csv
│ ├── 7_12.csv
│ ├── 7_13.csv
│ ├── 7_14.csv
│ ├── 7_2.csv
│ ├── 7_3.csv
│ ├── 7_4.csv
│ ├── 7_5.csv
│ ├── 7_6.csv
│ ├── 7_7.csv
│ ├── 7_8.csv
│ ├── 7_9.csv
│ ├── 80_1.csv
│ ├── 80_10.csv
│ ├── 80_11.csv
│ ├── 80_12.csv
│ ├── 80_13.csv
│ ├── 80_14.csv
│ ├── 80_2.csv
│ ├── 80_3.csv
│ ├── 80_4.csv
│ ├── 80_5.csv
│ ├── 80_6.csv
│ ├── 80_7.csv
│ ├── 80_8.csv
│ ├── 80_9.csv
│ ├── 81_1.csv
│ ├── 81_10.csv
│ ├── 81_11.csv
│ ├── 81_12.csv
│ ├── 81_13.csv
│ ├── 81_14.csv
│ ├── 81_2.csv
│ ├── 81_3.csv
│ ├── 81_4.csv
│ ├── 81_5.csv
│ ├── 81_6.csv
│ ├── 81_7.csv
│ ├── 81_8.csv
│ ├── 81_9.csv
│ ├── 82_1.csv
│ ├── 82_10.csv
│ ├── 82_11.csv
│ ├── 82_12.csv
│ ├── 82_13.csv
│ ├── 82_14.csv
│ ├── 82_2.csv
│ ├── 82_3.csv
│ ├── 82_4.csv
│ ├── 82_5.csv
│ ├── 82_6.csv
│ ├── 82_7.csv
│ ├── 82_8.csv
│ ├── 82_9.csv
│ ├── 83_1.csv
│ ├── 83_10.csv
│ ├── 83_11.csv
│ ├── 83_12.csv
│ ├── 83_13.csv
│ ├── 83_14.csv
│ ├── 83_2.csv
│ ├── 83_3.csv
│ ├── 83_4.csv
│ ├── 83_5.csv
│ ├── 83_6.csv
│ ├── 83_7.csv
│ ├── 83_8.csv
│ ├── 83_9.csv
│ ├── 84_1.csv
│ ├── 84_10.csv
│ ├── 84_11.csv
│ ├── 84_12.csv
│ ├── 84_13.csv
│ ├── 84_14.csv
│ ├── 84_2.csv
│ ├── 84_3.csv
│ ├── 84_4.csv
│ ├── 84_5.csv
│ ├── 84_6.csv
│ ├── 84_7.csv
│ ├── 84_8.csv
│ ├── 84_9.csv
│ ├── 85_1.csv
│ ├── 85_10.csv
│ ├── 85_11.csv
│ ├── 85_12.csv
│ ├── 85_13.csv
│ ├── 85_14.csv
│ ├── 85_2.csv
│ ├── 85_3.csv
│ ├── 85_4.csv
│ ├── 85_5.csv
│ ├── 85_6.csv
│ ├── 85_7.csv
│ ├── 85_8.csv
│ ├── 85_9.csv
│ ├── 86_1.csv
│ ├── 86_10.csv
│ ├── 86_11.csv
│ ├── 86_12.csv
│ ├── 86_13.csv
│ ├── 86_14.csv
│ ├── 86_2.csv
│ ├── 86_3.csv
│ ├── 86_4.csv
│ ├── 86_5.csv
│ ├── 86_6.csv
│ ├── 86_7.csv
│ ├── 86_8.csv
│ ├── 86_9.csv
│ ├── 87_1.csv
│ ├── 87_10.csv
│ ├── 87_11.csv
│ ├── 87_12.csv
│ ├── 87_13.csv
│ ├── 87_14.csv
│ ├── 87_2.csv
│ ├── 87_3.csv
│ ├── 87_4.csv
│ ├── 87_5.csv
│ ├── 87_6.csv
│ ├── 87_7.csv
│ ├── 87_8.csv
│ ├── 87_9.csv
│ ├── 88_1.csv
│ ├── 88_10.csv
│ ├── 88_11.csv
│ ├── 88_12.csv
│ ├── 88_13.csv
│ ├── 88_14.csv
│ ├── 88_2.csv
│ ├── 88_3.csv
│ ├── 88_4.csv
│ ├── 88_5.csv
│ ├── 88_6.csv
│ ├── 88_7.csv
│ ├── 88_8.csv
│ ├── 88_9.csv
│ ├── 89_1.csv
│ ├── 89_10.csv
│ ├── 89_11.csv
│ ├── 89_12.csv
│ ├── 89_13.csv
│ ├── 89_14.csv
│ ├── 89_2.csv
│ ├── 89_3.csv
│ ├── 89_4.csv
│ ├── 89_5.csv
│ ├── 89_6.csv
│ ├── 89_7.csv
│ ├── 89_8.csv
│ ├── 89_9.csv
│ ├── 8_1.csv
│ ├── 8_10.csv
│ ├── 8_11.csv
│ ├── 8_12.csv
│ ├── 8_13.csv
│ ├── 8_14.csv
│ ├── 8_2.csv
│ ├── 8_3.csv
│ ├── 8_4.csv
│ ├── 8_5.csv
│ ├── 8_6.csv
│ ├── 8_7.csv
│ ├── 8_8.csv
│ ├── 8_9.csv
│ ├── 90_1.csv
│ ├── 90_10.csv
│ ├── 90_11.csv
│ ├── 90_12.csv
│ ├── 90_13.csv
│ ├── 90_14.csv
│ ├── 90_2.csv
│ ├── 90_3.csv
│ ├── 90_4.csv
│ ├── 90_5.csv
│ ├── 90_6.csv
│ ├── 90_7.csv
│ ├── 90_8.csv
│ ├── 90_9.csv
│ ├── 91_1.csv
│ ├── 91_10.csv
│ ├── 91_11.csv
│ ├── 91_12.csv
│ ├── 91_13.csv
│ ├── 91_14.csv
│ ├── 91_2.csv
│ ├── 91_3.csv
│ ├── 91_4.csv
│ ├── 91_5.csv
│ ├── 91_6.csv
│ ├── 91_7.csv
│ ├── 91_8.csv
│ ├── 91_9.csv
│ ├── 92_1.csv
│ ├── 92_10.csv
│ ├── 92_11.csv
│ ├── 92_12.csv
│ ├── 92_13.csv
│ ├── 92_14.csv
│ ├── 92_2.csv
│ ├── 92_3.csv
│ ├── 92_4.csv
│ ├── 92_5.csv
│ ├── 92_6.csv
│ ├── 92_7.csv
│ ├── 92_8.csv
│ ├── 92_9.csv
│ ├── 93_1.csv
│ ├── 93_10.csv
│ ├── 93_11.csv
│ ├── 93_12.csv
│ ├── 93_13.csv
│ ├── 93_14.csv
│ ├── 93_2.csv
│ ├── 93_3.csv
│ ├── 93_4.csv
│ ├── 93_5.csv
│ ├── 93_6.csv
│ ├── 93_7.csv
│ ├── 93_8.csv
│ ├── 93_9.csv
│ ├── 94_1.csv
│ ├── 94_10.csv
│ ├── 94_11.csv
│ ├── 94_12.csv
│ ├── 94_13.csv
│ ├── 94_14.csv
│ ├── 94_2.csv
│ ├── 94_3.csv
│ ├── 94_4.csv
│ ├── 94_5.csv
│ ├── 94_6.csv
│ ├── 94_7.csv
│ ├── 94_8.csv
│ ├── 94_9.csv
│ ├── 95_1.csv
│ ├── 95_10.csv
│ ├── 95_11.csv
│ ├── 95_12.csv
│ ├── 95_13.csv
│ ├── 95_14.csv
│ ├── 95_2.csv
│ ├── 95_3.csv
│ ├── 95_4.csv
│ ├── 95_5.csv
│ ├── 95_6.csv
│ ├── 95_7.csv
│ ├── 95_8.csv
│ ├── 95_9.csv
│ ├── 96_1.csv
│ ├── 96_10.csv
│ ├── 96_11.csv
│ ├── 96_12.csv
│ ├── 96_13.csv
│ ├── 96_14.csv
│ ├── 96_2.csv
│ ├── 96_3.csv
│ ├── 96_4.csv
│ ├── 96_5.csv
│ ├── 96_6.csv
│ ├── 96_7.csv
│ ├── 96_8.csv
│ ├── 96_9.csv
│ ├── 97_1.csv
│ ├── 97_10.csv
│ ├── 97_11.csv
│ ├── 97_12.csv
│ ├── 97_13.csv
│ ├── 97_14.csv
│ ├── 97_2.csv
│ ├── 97_3.csv
│ ├── 97_4.csv
│ ├── 97_5.csv
│ ├── 97_6.csv
│ ├── 97_7.csv
│ ├── 97_8.csv
│ ├── 97_9.csv
│ ├── 98_1.csv
│ ├── 98_10.csv
│ ├── 98_11.csv
│ ├── 98_12.csv
│ ├── 98_13.csv
│ ├── 98_14.csv
│ ├── 98_2.csv
│ ├── 98_3.csv
│ ├── 98_4.csv
│ ├── 98_5.csv
│ ├── 98_6.csv
│ ├── 98_7.csv
│ ├── 98_8.csv
│ ├── 98_9.csv
│ ├── 99_1.csv
│ ├── 99_10.csv
│ ├── 99_11.csv
│ ├── 99_12.csv
│ ├── 99_13.csv
│ ├── 99_14.csv
│ ├── 99_2.csv
│ ├── 99_3.csv
│ ├── 99_4.csv
│ ├── 99_5.csv
│ ├── 99_6.csv
│ ├── 99_7.csv
│ ├── 99_8.csv
│ ├── 99_9.csv
│ ├── 9_1.csv
│ ├── 9_10.csv
│ ├── 9_11.csv
│ ├── 9_12.csv
│ ├── 9_13.csv
│ ├── 9_14.csv
│ ├── 9_2.csv
│ ├── 9_3.csv
│ ├── 9_4.csv
│ ├── 9_5.csv
│ ├── 9_6.csv
│ ├── 9_7.csv
│ ├── 9_8.csv
│ └── 9_9.csv
├── static/
│ ├── css/
│ │ ├── alliance_station_display.css
│ │ ├── audience_display.css
│ │ ├── bracket_display.css
│ │ ├── cheesy-arena.css
│ │ ├── field_monitor_display.css
│ │ ├── fonts/
│ │ │ ├── futura-lt-bold.otf
│ │ │ └── futura-lt.otf
│ │ ├── logo_display.css
│ │ ├── placeholder_display.css
│ │ ├── queueing_display.css
│ │ ├── rankings_display.css
│ │ ├── referee_panel.css
│ │ ├── scoring_panel.css
│ │ ├── twitch_display.css
│ │ ├── wall_display.css
│ │ └── webpage_display.css
│ ├── js/
│ │ ├── alliance_selection.js
│ │ ├── alliance_station_display.js
│ │ ├── announcer_display.js
│ │ ├── audience_display.js
│ │ ├── bracket_display.js
│ │ ├── cheesy-common.js
│ │ ├── cheesy-websocket.js
│ │ ├── field_monitor_display.js
│ │ ├── lib/
│ │ │ ├── handlebars-1.3.0.js
│ │ │ └── jquery.websocket-0.0.1.js
│ │ ├── logo_display.js
│ │ ├── lower_thirds.js
│ │ ├── match_play.js
│ │ ├── match_review.js
│ │ ├── match_timing.js
│ │ ├── placeholder_display.js
│ │ ├── queueing_display.js
│ │ ├── rankings_display.js
│ │ ├── referee_panel.js
│ │ ├── scoring_panel.js
│ │ ├── setup_displays.js
│ │ ├── setup_field_testing.js
│ │ ├── setup_schedule.js
│ │ ├── twitch_display.js
│ │ ├── wall_display.js
│ │ └── webpage_display.js
│ └── manifest/
│ ├── blue_far_scoring.manifest
│ ├── blue_near_scoring.manifest
│ ├── red_far_scoring.manifest
│ ├── red_near_scoring.manifest
│ └── referee.manifest
├── switch_config.txt
├── templates/
│ ├── alliance_selection.html
│ ├── alliance_station_display.html
│ ├── announcer_display.html
│ ├── announcer_display_match_load.html
│ ├── announcer_display_score_posted.html
│ ├── audience_display.html
│ ├── audience_display_radio_buttons.html
│ ├── backups.csv
│ ├── base.html
│ ├── bracket_display.html
│ ├── bracket_report.html
│ ├── edit_match_result.html
│ ├── edit_team.html
│ ├── field_monitor_display.html
│ ├── fta.csv
│ ├── index.html
│ ├── login.html
│ ├── logo_display.html
│ ├── match_logs.html
│ ├── match_play.html
│ ├── match_play_match_load.html
│ ├── match_review.html
│ ├── placeholder_display.html
│ ├── queueing_display.html
│ ├── queueing_display_match_load.html
│ ├── rankings.csv
│ ├── rankings_display.html
│ ├── referee_panel.html
│ ├── referee_panel_foul_list.html
│ ├── schedule.csv
│ ├── scoring_panel.html
│ ├── setup_awards.html
│ ├── setup_breaks.html
│ ├── setup_displays.html
│ ├── setup_field_testing.html
│ ├── setup_judging.html
│ ├── setup_lower_thirds.html
│ ├── setup_schedule.html
│ ├── setup_settings.html
│ ├── setup_sponsor_slides.html
│ ├── setup_teams.html
│ ├── teams.csv
│ ├── twitch_display.html
│ ├── view_match_log.html
│ ├── wall_display.html
│ └── webpage_display.html
├── tournament/
│ ├── awards.go
│ ├── awards_test.go
│ ├── judging_schedule.go
│ ├── judging_schedule_test.go
│ ├── qualification_rankings.go
│ ├── qualification_rankings_test.go
│ ├── schedule.go
│ ├── schedule_test.go
│ └── test_helpers.go
├── tunnel
├── tunnel_nginx_config
├── web/
│ ├── alliance_selection.go
│ ├── alliance_selection_test.go
│ ├── alliance_station_display.go
│ ├── alliance_station_display_test.go
│ ├── announcer_display.go
│ ├── announcer_display_test.go
│ ├── api.go
│ ├── api_test.go
│ ├── audience_display.go
│ ├── audience_display_test.go
│ ├── bracket_display.go
│ ├── bracket_display_test.go
│ ├── display_utils.go
│ ├── field_monitor_display.go
│ ├── field_monitor_display_test.go
│ ├── login.go
│ ├── login_test.go
│ ├── logo_display.go
│ ├── logo_display_test.go
│ ├── match_logs.go
│ ├── match_play.go
│ ├── match_play_test.go
│ ├── match_review.go
│ ├── match_review_test.go
│ ├── placeholder_display.go
│ ├── placeholder_display_test.go
│ ├── queueing_display.go
│ ├── queueing_display_test.go
│ ├── rankings_display.go
│ ├── rankings_display_test.go
│ ├── referee_panel.go
│ ├── referee_panel_test.go
│ ├── reports.go
│ ├── reports_test.go
│ ├── scoring_panel.go
│ ├── scoring_panel_test.go
│ ├── setup_awards.go
│ ├── setup_awards_test.go
│ ├── setup_breaks.go
│ ├── setup_breaks_test.go
│ ├── setup_displays.go
│ ├── setup_displays_test.go
│ ├── setup_field_testing.go
│ ├── setup_field_testing_test.go
│ ├── setup_judging.go
│ ├── setup_judging_test.go
│ ├── setup_lower_thirds.go
│ ├── setup_lower_thirds_test.go
│ ├── setup_schedule.go
│ ├── setup_schedule_test.go
│ ├── setup_settings.go
│ ├── setup_settings_test.go
│ ├── setup_sponsor_slides.go
│ ├── setup_sponsor_slides_test.go
│ ├── setup_teams.go
│ ├── setup_teams_test.go
│ ├── twitch_display.go
│ ├── twitch_display_test.go
│ ├── wall_display.go
│ ├── wall_display_test.go
│ ├── web.go
│ ├── web_test.go
│ ├── webpage_display.go
│ └── webpage_display_test.go
└── websocket/
├── notifier.go
├── notifier_test.go
├── websocket.go
└── websocket_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = false
max_line_length = 120
tab_width = 2
ij_continuation_indent_size = 2
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = true
ij_smart_tabs = false
ij_visual_guides =
ij_wrap_on_typing = false
[*.css]
ij_css_align_closing_brace_with_properties = false
ij_css_blank_lines_around_nested_selector = 1
ij_css_blank_lines_between_blocks = 0
ij_css_block_comment_add_space = false
ij_css_brace_placement = end_of_line
ij_css_enforce_quotes_on_format = false
ij_css_hex_color_long_format = false
ij_css_hex_color_lower_case = false
ij_css_hex_color_short_format = false
ij_css_hex_color_upper_case = false
ij_css_keep_blank_lines_in_code = 2
ij_css_keep_indents_on_empty_lines = false
ij_css_keep_single_line_blocks = false
ij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow
ij_css_space_after_colon = true
ij_css_space_before_opening_brace = true
ij_css_use_double_quotes = true
ij_css_value_alignment = do_not_align
[*.js]
ij_javascript_align_imports = false
ij_javascript_align_multiline_array_initializer_expression = false
ij_javascript_align_multiline_binary_operation = false
ij_javascript_align_multiline_chained_methods = false
ij_javascript_align_multiline_extends_list = false
ij_javascript_align_multiline_for = true
ij_javascript_align_multiline_parameters = true
ij_javascript_align_multiline_parameters_in_calls = false
ij_javascript_align_multiline_ternary_operation = false
ij_javascript_align_object_properties = 0
ij_javascript_align_union_types = false
ij_javascript_align_var_statements = 0
ij_javascript_array_initializer_new_line_after_left_brace = false
ij_javascript_array_initializer_right_brace_on_new_line = false
ij_javascript_array_initializer_wrap = off
ij_javascript_assignment_wrap = off
ij_javascript_binary_operation_sign_on_next_line = false
ij_javascript_binary_operation_wrap = off
ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/**
ij_javascript_blank_lines_after_imports = 1
ij_javascript_blank_lines_around_class = 1
ij_javascript_blank_lines_around_field = 0
ij_javascript_blank_lines_around_function = 1
ij_javascript_blank_lines_around_method = 1
ij_javascript_block_brace_style = end_of_line
ij_javascript_block_comment_add_space = false
ij_javascript_block_comment_at_first_column = true
ij_javascript_call_parameters_new_line_after_left_paren = false
ij_javascript_call_parameters_right_paren_on_new_line = false
ij_javascript_call_parameters_wrap = off
ij_javascript_catch_on_new_line = false
ij_javascript_chained_call_dot_on_new_line = true
ij_javascript_class_brace_style = end_of_line
ij_javascript_class_decorator_wrap = split_into_lines
ij_javascript_class_field_decorator_wrap = off
ij_javascript_class_method_decorator_wrap = off
ij_javascript_comma_on_new_line = false
ij_javascript_do_while_brace_force = never
ij_javascript_else_on_new_line = false
ij_javascript_enforce_trailing_comma = keep
ij_javascript_extends_keyword_wrap = off
ij_javascript_extends_list_wrap = off
ij_javascript_field_prefix = _
ij_javascript_file_name_style = relaxed
ij_javascript_finally_on_new_line = false
ij_javascript_for_brace_force = never
ij_javascript_for_statement_new_line_after_left_paren = false
ij_javascript_for_statement_right_paren_on_new_line = false
ij_javascript_for_statement_wrap = off
ij_javascript_force_quote_style = false
ij_javascript_force_semicolon_style = false
ij_javascript_function_expression_brace_style = end_of_line
ij_javascript_function_parameter_decorator_wrap = off
ij_javascript_if_brace_force = never
ij_javascript_import_merge_members = global
ij_javascript_import_prefer_absolute_path = global
ij_javascript_import_sort_members = true
ij_javascript_import_sort_module_name = false
ij_javascript_import_use_node_resolution = true
ij_javascript_imports_wrap = on_every_item
ij_javascript_indent_case_from_switch = true
ij_javascript_indent_chained_calls = true
ij_javascript_indent_package_children = 0
ij_javascript_jsx_attribute_value = braces
ij_javascript_keep_blank_lines_in_code = 2
ij_javascript_keep_first_column_comment = true
ij_javascript_keep_indents_on_empty_lines = false
ij_javascript_keep_line_breaks = true
ij_javascript_keep_simple_blocks_in_one_line = false
ij_javascript_keep_simple_methods_in_one_line = false
ij_javascript_line_comment_add_space = true
ij_javascript_line_comment_at_first_column = false
ij_javascript_method_brace_style = end_of_line
ij_javascript_method_call_chain_wrap = off
ij_javascript_method_parameters_new_line_after_left_paren = false
ij_javascript_method_parameters_right_paren_on_new_line = false
ij_javascript_method_parameters_wrap = off
ij_javascript_object_literal_wrap = on_every_item
ij_javascript_object_types_wrap = on_every_item
ij_javascript_parentheses_expression_new_line_after_left_paren = false
ij_javascript_parentheses_expression_right_paren_on_new_line = false
ij_javascript_place_assignment_sign_on_next_line = false
ij_javascript_prefer_as_type_cast = false
ij_javascript_prefer_explicit_types_function_expression_returns = false
ij_javascript_prefer_explicit_types_function_returns = false
ij_javascript_prefer_explicit_types_vars_fields = false
ij_javascript_prefer_parameters_wrap = false
ij_javascript_property_prefix =
ij_javascript_reformat_c_style_comments = false
ij_javascript_space_after_colon = true
ij_javascript_space_after_comma = true
ij_javascript_space_after_dots_in_rest_parameter = false
ij_javascript_space_after_generator_mult = true
ij_javascript_space_after_property_colon = true
ij_javascript_space_after_quest = true
ij_javascript_space_after_type_colon = true
ij_javascript_space_after_unary_not = false
ij_javascript_space_before_async_arrow_lparen = true
ij_javascript_space_before_catch_keyword = true
ij_javascript_space_before_catch_left_brace = true
ij_javascript_space_before_catch_parentheses = true
ij_javascript_space_before_class_lbrace = true
ij_javascript_space_before_class_left_brace = true
ij_javascript_space_before_colon = true
ij_javascript_space_before_comma = false
ij_javascript_space_before_do_left_brace = true
ij_javascript_space_before_else_keyword = true
ij_javascript_space_before_else_left_brace = true
ij_javascript_space_before_finally_keyword = true
ij_javascript_space_before_finally_left_brace = true
ij_javascript_space_before_for_left_brace = true
ij_javascript_space_before_for_parentheses = true
ij_javascript_space_before_for_semicolon = false
ij_javascript_space_before_function_left_parenth = true
ij_javascript_space_before_generator_mult = false
ij_javascript_space_before_if_left_brace = true
ij_javascript_space_before_if_parentheses = true
ij_javascript_space_before_method_call_parentheses = false
ij_javascript_space_before_method_left_brace = true
ij_javascript_space_before_method_parentheses = false
ij_javascript_space_before_property_colon = false
ij_javascript_space_before_quest = true
ij_javascript_space_before_switch_left_brace = true
ij_javascript_space_before_switch_parentheses = true
ij_javascript_space_before_try_left_brace = true
ij_javascript_space_before_type_colon = false
ij_javascript_space_before_unary_not = false
ij_javascript_space_before_while_keyword = true
ij_javascript_space_before_while_left_brace = true
ij_javascript_space_before_while_parentheses = true
ij_javascript_spaces_around_additive_operators = true
ij_javascript_spaces_around_arrow_function_operator = true
ij_javascript_spaces_around_assignment_operators = true
ij_javascript_spaces_around_bitwise_operators = true
ij_javascript_spaces_around_equality_operators = true
ij_javascript_spaces_around_logical_operators = true
ij_javascript_spaces_around_multiplicative_operators = true
ij_javascript_spaces_around_relational_operators = true
ij_javascript_spaces_around_shift_operators = true
ij_javascript_spaces_around_unary_operator = false
ij_javascript_spaces_within_array_initializer_brackets = false
ij_javascript_spaces_within_brackets = false
ij_javascript_spaces_within_catch_parentheses = false
ij_javascript_spaces_within_for_parentheses = false
ij_javascript_spaces_within_if_parentheses = false
ij_javascript_spaces_within_imports = false
ij_javascript_spaces_within_interpolation_expressions = false
ij_javascript_spaces_within_method_call_parentheses = false
ij_javascript_spaces_within_method_parentheses = false
ij_javascript_spaces_within_object_literal_braces = false
ij_javascript_spaces_within_object_type_braces = true
ij_javascript_spaces_within_parentheses = false
ij_javascript_spaces_within_switch_parentheses = false
ij_javascript_spaces_within_type_assertion = false
ij_javascript_spaces_within_union_types = true
ij_javascript_spaces_within_while_parentheses = false
ij_javascript_special_else_if_treatment = true
ij_javascript_ternary_operation_signs_on_next_line = false
ij_javascript_ternary_operation_wrap = off
ij_javascript_union_types_wrap = on_every_item
ij_javascript_use_chained_calls_group_indents = false
ij_javascript_use_double_quotes = true
ij_javascript_use_explicit_js_extension = auto
ij_javascript_use_import_type = auto
ij_javascript_use_path_mapping = always
ij_javascript_use_public_modifier = false
ij_javascript_use_semicolon_after_statement = true
ij_javascript_var_declaration_wrap = normal
ij_javascript_while_brace_force = never
ij_javascript_while_on_new_line = false
ij_javascript_wrap_comments = false
[*.go]
indent_style = tab
tab_width = 4
ij_go_GROUP_CURRENT_PROJECT_IMPORTS = false
ij_go_add_leading_space_to_comments = true
ij_go_add_parentheses_for_single_import = false
ij_go_call_parameters_new_line_after_left_paren = true
ij_go_call_parameters_right_paren_on_new_line = true
ij_go_call_parameters_wrap = on_every_item
ij_go_fill_paragraph_width = 80
ij_go_group_stdlib_imports = false
ij_go_import_sorting = gofmt
ij_go_keep_indents_on_empty_lines = false
ij_go_local_group_mode = project
ij_go_local_package_prefixes =
ij_go_move_all_imports_in_one_declaration = false
ij_go_move_all_stdlib_imports_in_one_group = false
ij_go_remove_redundant_import_aliases = false
ij_go_run_go_fmt_on_reformat = true
ij_go_use_back_quotes_for_imports = false
ij_go_wrap_comp_lit = on_every_item
ij_go_wrap_comp_lit_newline_after_lbrace = true
ij_go_wrap_comp_lit_newline_before_rbrace = true
ij_go_wrap_func_params = on_every_item
ij_go_wrap_func_params_newline_after_lparen = true
ij_go_wrap_func_params_newline_before_rparen = true
ij_go_wrap_func_result = on_every_item
ij_go_wrap_func_result_newline_after_lparen = true
ij_go_wrap_func_result_newline_before_rparen = true
[{*.htm,*.html}]
ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3
ij_html_align_attributes = false
ij_html_align_text = false
ij_html_attribute_wrap = normal
ij_html_block_comment_add_space = false
ij_html_block_comment_at_first_column = true
ij_html_do_not_align_children_of_min_lines = 0
ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p
ij_html_do_not_indent_children_of_tags =
ij_html_enforce_quotes = false
ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var
ij_html_keep_blank_lines = 2
ij_html_keep_indents_on_empty_lines = false
ij_html_keep_line_breaks = true
ij_html_keep_line_breaks_in_text = true
ij_html_keep_whitespaces = false
ij_html_keep_whitespaces_inside = span, pre, textarea
ij_html_line_comment_at_first_column = true
ij_html_new_line_after_last_attribute = never
ij_html_new_line_before_first_attribute = never
ij_html_quote_style = double
ij_html_remove_new_line_before_tags = br
ij_html_space_after_tag_name = false
ij_html_space_around_equality_in_attribute = false
ij_html_space_inside_empty_tag = false
ij_html_text_wrap = normal
[{*.markdown,*.md}]
ij_markdown_force_one_space_after_blockquote_symbol = true
ij_markdown_force_one_space_after_header_symbol = true
ij_markdown_force_one_space_after_list_bullet = true
ij_markdown_force_one_space_between_words = true
ij_markdown_format_tables = true
ij_markdown_insert_quote_arrows_on_wrap = true
ij_markdown_keep_indents_on_empty_lines = false
ij_markdown_keep_line_breaks_inside_text_blocks = true
ij_markdown_max_lines_around_block_elements = 1
ij_markdown_max_lines_around_header = 1
ij_markdown_max_lines_between_paragraphs = 1
ij_markdown_min_lines_around_block_elements = 1
ij_markdown_min_lines_around_header = 1
ij_markdown_min_lines_between_paragraphs = 1
ij_markdown_wrap_text_if_long = true
ij_markdown_wrap_text_inside_blockquotes = true
================================================
FILE: .github/workflows/release.yml
================================================
on:
push:
tags:
- "v*"
name: Create Release
jobs:
create_release:
runs-on: ubuntu-latest
env:
ASSET_FILES: LICENSE README.md access_point_config.tar.gz fix_avatar_colors_for_overlay font schedules static
switch_config.txt templates tunnel
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.22.x
- name: Check out code
uses: actions/checkout@v2
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Cheesy Arena ${{ github.ref }}
body: This is a release of Cheesy Arena for the 2025 FRC game, REEFSCAPE. Download the version for your
operating system below. Supported operating systems are Linux, macOS (x64 and M1), and Windows.
draft: false
prerelease: false
- name: Set additional environment variables
run: |
echo "LINUX_X64_FILENAME=cheesy-arena.${GITHUB_REF:10}.linux.x64.zip" >> $GITHUB_ENV
echo "MACOS_X64_FILENAME=cheesy-arena.${GITHUB_REF:10}.macos.x64.zip" >> $GITHUB_ENV
echo "MACOS_M1_FILENAME=cheesy-arena.${GITHUB_REF:10}.macos.m1.zip" >> $GITHUB_ENV
echo "WINDOWS_X64_FILENAME=cheesy-arena.${GITHUB_REF:10}.windows.x64.zip" >> $GITHUB_ENV
- name: Build Linux bundle
run: |
rm -rf cheesy-arena*
mkdir cheesy-arena
GOOS=linux GOARCH=amd64 go build -o cheesy-arena/
cp -r ${{ env.ASSET_FILES }} cheesy-arena/
zip -r -X ${{ env.LINUX_X64_FILENAME }} cheesy-arena
- name: Upload Linux bundle
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.LINUX_X64_FILENAME }}
asset_name: ${{ env.LINUX_X64_FILENAME }}
asset_content_type: application/zip
- name: Build MacOS x64 bundle
run: |
rm -rf cheesy-arena*
mkdir cheesy-arena
GOOS=darwin GOARCH=amd64 go build -o cheesy-arena/
cp -r ${{ env.ASSET_FILES }} cheesy-arena/
zip -r -X ${{ env.MACOS_X64_FILENAME }} cheesy-arena
- name: Upload MacOS x64 bundle
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.MACOS_X64_FILENAME }}
asset_name: ${{ env.MACOS_X64_FILENAME }}
asset_content_type: application/zip
- name: Build MacOS M1 bundle
run: |
rm -rf cheesy-arena*
mkdir cheesy-arena
GOOS=darwin GOARCH=arm64 go build -o cheesy-arena/
cp -r ${{ env.ASSET_FILES }} cheesy-arena/
zip -r -X ${{ env.MACOS_M1_FILENAME }} cheesy-arena
- name: Upload MacOS M1 bundle
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.MACOS_M1_FILENAME }}
asset_name: ${{ env.MACOS_M1_FILENAME }}
asset_content_type: application/zip
- name: Build Windows bundle
run: |
rm -rf cheesy-arena*
mkdir cheesy-arena
GOOS=windows GOARCH=amd64 go build -o cheesy-arena/
cp -r ${{ env.ASSET_FILES }} cheesy-arena/
zip -r -X ${{ env.WINDOWS_X64_FILENAME }} cheesy-arena
- name: Upload Windows bundle
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.WINDOWS_X64_FILENAME }}
asset_name: ${{ env.WINDOWS_X64_FILENAME }}
asset_content_type: application/zip
================================================
FILE: .github/workflows/test.yml
================================================
on: [ push, pull_request ]
name: Build/Test
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.22.x
- name: Check out code
uses: actions/checkout@v2
- name: Build
run: go build
- name: Test
run: go test ./...
- name: Check formatting
run: test -z "$(go fmt ./...)"
================================================
FILE: .gitignore
================================================
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
cheesy-arena
# Folders
_obj
_test
.idea
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.db
*.db-journal
*.out
.DS_Store
static/logs
static/img/avatars
================================================
FILE: AGENTS.md
================================================
# Repository Guidelines
## Project Structure & Module Organization
`main.go` is the entry point for the Go web server. Core domains live in top-level packages such as `field/`, `game/`, `network/`, `partner/`, `playoff/`, `tournament/`, and `websocket/`. Web UI assets are in `web/`, `static/`, and `templates/`. Pre-generated schedules are in `schedules/`. BoltDB data is stored in `db/` (and test fixtures in `*_test.db` files at the repo root).
## Build, Test, and Development Commands
Use Go 1.23+ (see `go.mod`).
1. `go build`
Builds the `cheesy-arena` binary in the repo root.
1. `./cheesy-arena`
Runs the server; open `http://localhost:8080` in a browser.
1. `go test ./...`
Runs all Go tests across packages.
## Coding Style & Naming Conventions
Follow standard Go style: tabs for indentation, exported names in `CamelCase`, unexported in `camelCase`. Format code with `gofmt` before submitting changes. Keep package names short and domain-focused (matching existing directories like `field`, `game`, `partner`).
## Testing Guidelines
Tests are Go `*_test.go` files co-located with packages (for example `field/`, `game/`, `partner/`, `playoff/`). Use `go test ./...` for the full suite and `go test ./field -run TestName` to target specific areas. When adding new behavior, add or update tests in the same package and prefer table-driven tests for coverage.
## Commit & Pull Request Guidelines
Commit messages in this repo are short, imperative sentences (for example “Fix driver station TCP reads”) and often include an issue/PR number in parentheses (for example “... (#258)”). Keep to that style.
PRs should include:
1. A clear summary of the change.
1. Test notes (exact commands run, for example `go test ./...`).
1. UI screenshots when changing pages in `web/`, `static/`, or `templates/`.
## Configuration & Ops Notes
Cheesy Arena is designed to run as a local web server and uses BoltDB for data. For field networking and hardware integrations, see the project README and relevant `field/` or `plc/` code before making behavioral changes.
================================================
FILE: LICENSE
================================================
Copyright (c) 2014, Team 254
All rights reserved.
This software may be used and redistributed subject to the following conditions:
1. The software may be used without restriction for testing, scrimmages,
off-season events, and for evaluation purposes.
2. The software may be modified for such use, but the modifications may not be
redistributed without permission from Team 254.
3. Redistribution for the purpose of contributing to the original project (e.g.
forking on GitHub and submitting pull requests) is permitted.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
Cheesy Arena [](https://github.com/Team254/cheesy-arena/actions)
============
A field management system that just works.
For the game-agnostic version, see [Cheesy Arena Lite](https://github.com/Team254/cheesy-arena-lite).
## Key features
**For participants and spectators**
* Same network isolation and security as the official FIRST FMS
* No-lag realtime scoring
* Team stack lights and seven-segment display are replaced by an LCD screen, which shows team info before the match and
realtime scoring and timer during the match
* Smooth-scrolling rankings display
* Direct publishing of schedule, results, and rankings to The Blue Alliance
**For scorekeepers and event staff**
* Runs on Windows, macOS, and Linux
* No install prerequisites
* No "pre-start" – hardware is configured automatically and in the background
* Flexible and quick match schedule generation
* Streamlined realtime score entry
* Reports, results, and logs can be viewed from any computer
* An arbitrary number of auxiliary displays can be set up using any computer with just a web browser, to show rankings,
queueing, field status, etc.
## License
Teams may use Cheesy Arena freely for practice, scrimmages, and off-season events. See [LICENSE](LICENSE) for more
details.
## Installing
**From a pre-built release**
Download the [latest release](https://github.com/Team254/cheesy-arena/releases). Pre-built packages are available for
Linux, macOS (x64 and M1), and Windows.
On recent versions of macOS, you may be prevented from running an app from an unidentified developer;
see [these instructions](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac)
on how to bypass the warning.
**From source**
1. Download [Go](https://golang.org/dl/) (version 1.22 or later required)
1. Clone this GitHub repository to a location of your choice
1. Navigate to the repository's directory in the terminal
1. Compile the code with `go build`
1. Run the `cheesy-arena` or `cheesy-arena.exe` binary
1. Navigate to http://localhost:8080 in your browser (Google Chrome recommended)
**IP address configuration**
When running Cheesy Arena on a playing field with robots, set the IP address of the computer running Cheesy Arena to
10.0.100.5. By a convention baked into the FRC Driver Station software, driver stations will broadcast their presence on
the network to this hardcoded address so that the FMS does not need to discover them by some other method.
When running Cheesy Arena without robots for testing or development, any IP address can be used.
## Under the hood
Cheesy Arena is written using [Go](https://golang.org), a language developed by Google and first released in 2009. Go
excels in the areas of concurrency, networking, performance, and portability, which makes it ideal for a field
management system.
Cheesy Arena is implemented as a web server, with all human interaction done via browser. The graphical interfaces are
implemented in HTML, JavaScript, and CSS. There are many advantages to this approach – development of new
graphical elements is rapid, and no software needs to be installed other than on the server. Client web pages send
commands and receive updates using WebSockets.
[Bolt](https://github.com/etcd-io/bbolt) is used as the datastore, and making backups or transferring data from one
installation to another is as simple as copying the database file.
Schedule generation is fast because pre-generated schedules are included with the code. Each schedule contains a certain
number of matches per team for placeholder teams 1 through N, so generating the actual match schedule becomes a simple
exercise in permuting the mapping of real teams to placeholder teams. The pre-generated schedules are checked into this
repository and can be vetted in advance of any events for deviations from the randomness (and other) requirements.
Cheesy Arena includes support for, but doesn't require, networking hardware similar to that used in official FRC events.
Teams are issued their own SSIDs and WPA keys, and when connected to Cheesy Arena are isolated to a VLAN which prevents
any communication other than between the driver station, robot, and event server. The network hardware is reconfigured
via SSH and Telnet commands for the new set of teams when each mach is loaded.
## PLC integration
Cheesy Arena has the ability to integrate with an Allen-Bradley PLC setup similar to the one that FIRST uses, to read
field sensors and control lights and motors. The PLC hardware travels with the FIRST California fields; contact your FTA
for more information.
The PLC code can be found [here](https://github.com/ejordan376/Cheesy-PLC).
## Team Sign integration
Cheesy Arena has the ability to integrate with
the [Cypress Team Signs](https://cypressintegration.com/customsolutions/teamdisplay/) used at official FRC events. See
the [Configuring Cheesy Arena wiki page](https://github.com/Team254/cheesy-arena/wiki/Configuring-Cheesy-Arena-Settings#team-signs)
for details configurating the team signs in Cheesy Arena.
## LED hardware
Due to the prohibitive cost of the LEDs and LED controllers used on official fields, for years in which LEDs are
mandatory for a proper game experience (such as 2018), Cheesy Arena integrates
with [Advatek](https://www.advateklights.com) controllers and LEDs.
## Advanced networking
See the [Advanced Networking wiki page](https://github.com/Team254/cheesy-arena/wiki/Advanced-Networking-Concepts) for
instructions on what equipment to obtain and how to configure it in order to support advanced network security.
## Contributing
Cheesy Arena is far from finished! You can help by:
* Writing a missing feature, and sending a pull request
* Filing any bugs or feature requests using the [issue tracker](https://github.com/Team254/cheesy-arena/issues)
* Contributing documentation to the [wiki](https://github.com/Team254/cheesy-arena/wiki)
* Sending baked goods to [Pat](https://github.com/patfair)
## Acknowledgements
[Several folks](https://github.com/Team254/cheesy-arena/graphs/contributors) have contributed pull requests. Thanks!
In addition, the following individuals have contributed to make Cheesy Arena a reality:
* Tom Bottiglieri
* James Cerar
* Kiet Chau
* Travis Covington
* Nick Eyre
* Patrick Fairbank
* Eugene Fang
* Thad House
* Ed Jordan
* Karthik Kanagasabapathy
* Ken Mitchell
* Andrew Nabors
* Jared Russell
* Ken Schenke
* Austin Schuh
* Colin Wilson
================================================
FILE: coverage
================================================
go test -coverprofile=coverage.out ./... && sleep 1 && go tool cover -html=coverage.out
================================================
FILE: field/arena.go
================================================
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Functions for controlling the arena and match play.
package field
import (
"fmt"
"log"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/Team254/cheesy-arena/game"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/network"
"github.com/Team254/cheesy-arena/partner"
"github.com/Team254/cheesy-arena/playoff"
"github.com/Team254/cheesy-arena/plc"
)
const (
arenaLoopPeriodMs = 10
arenaLoopWarningMs = 5
dsPacketPeriodMs = 500
dsPacketWarningMs = 550
periodicTaskPeriodSec = 30
matchEndScoreDwellSec = 3
postTimeoutSec = 4
preLoadNextMatchDelaySec = 5
scheduledBreakDelaySec = 5
earlyLateThresholdMin = 2.5
MaxMatchGapMin = 20
)
// Progression of match states.
type MatchState int
const (
PreMatch MatchState = iota
StartMatch
WarmupPeriod
AutoPeriod
PausePeriod
TeleopPeriod
PostMatch
TimeoutActive
PostTimeout
)
type Arena struct {
Database *model.Database
EventSettings *model.EventSettings
accessPoint network.AccessPoint
networkSwitch *network.Switch
redSCC *network.SCCSwitch
blueSCC *network.SCCSwitch
Plc plc.Plc
TbaClient *partner.TbaClient
NexusClient *partner.NexusClient
BlackmagicClient *partner.BlackmagicClient
CompanionClient *partner.CompanionClient
AllianceStations map[string]*AllianceStation
Displays map[string]*Display
TeamSigns *TeamSigns
ScoringPanelRegistry
ArenaNotifiers
MatchState
lastMatchState MatchState
CurrentMatch *model.Match
MatchStartTime time.Time
LastMatchTimeSec float64
RedRealtimeScore *RealtimeScore
BlueRealtimeScore *RealtimeScore
lastDsPacketTime time.Time
lastPeriodicTaskTime time.Time
EventStatus EventStatus
FieldVolunteers bool
FieldReset bool
AudienceDisplayMode string
SavedMatch *model.Match
SavedMatchResult *model.MatchResult
SavedRankings game.Rankings
AllianceStationDisplayMode string
AllianceSelectionAlliances []model.Alliance
AllianceSelectionRankedTeams []model.AllianceSelectionRankedTeam
AllianceSelectionShowTimer bool
AllianceSelectionTimeRemainingSec int
PlayoffTournament *playoff.PlayoffTournament
LowerThird *model.LowerThird
ShowLowerThird bool
MuteMatchSounds bool
matchAborted bool
soundsPlayed map[*game.MatchSound]struct{}
breakDescription string
preloadedTeams *[6]*model.Team
NextFoulId int
}
type AllianceStation struct {
DsConn *DriverStationConnection
Ethernet bool
AStop bool
EStop bool
Bypass bool
Team *model.Team
WifiStatus network.TeamWifiStatus
aStopReset bool
GameData string
}
// Creates the arena and sets it to its initial state.
func NewArena(dbPath string) (*Arena, error) {
arena := new(Arena)
arena.configureNotifiers()
arena.Plc = new(plc.ModbusPlc)
arena.AllianceStations = make(map[string]*AllianceStation)
arena.AllianceStations["R1"] = new(AllianceStation)
arena.AllianceStations["R2"] = new(AllianceStation)
arena.AllianceStations["R3"] = new(AllianceStation)
arena.AllianceStations["B1"] = new(AllianceStation)
arena.AllianceStations["B2"] = new(AllianceStation)
arena.AllianceStations["B3"] = new(AllianceStation)
arena.Displays = make(map[string]*Display)
arena.TeamSigns = NewTeamSigns()
var err error
arena.Database, err = model.OpenDatabase(dbPath)
if err != nil {
return nil, err
}
err = arena.LoadSettings()
if err != nil {
return nil, err
}
arena.ScoringPanelRegistry.initialize()
// Load empty match as current.
arena.MatchState = PreMatch
arena.LoadTestMatch()
arena.LastMatchTimeSec = 0
arena.lastMatchState = -1
// Initialize display parameters.
arena.AudienceDisplayMode = "blank"
arena.SavedMatch = &model.Match{}
arena.SavedMatchResult = model.NewMatchResult()
arena.AllianceStationDisplayMode = "match"
return arena, nil
}
// Loads or reloads the event settings upon initial setup or change.
func (arena *Arena) LoadSettings() error {
settings, err := arena.Database.GetEventSettings()
if err != nil {
return err
}
arena.EventSettings = settings
// Initialize the components that depend on settings.
arena.TeamSigns.Red1.SetId(settings.TeamSignRed1Id)
arena.TeamSigns.Red2.SetId(settings.TeamSignRed2Id)
arena.TeamSigns.Red3.SetId(settings.TeamSignRed3Id)
arena.TeamSigns.RedTimer.SetId(settings.TeamSignRedTimerId)
arena.TeamSigns.Blue1.SetId(settings.TeamSignBlue1Id)
arena.TeamSigns.Blue2.SetId(settings.TeamSignBlue2Id)
arena.TeamSigns.Blue3.SetId(settings.TeamSignBlue3Id)
arena.TeamSigns.BlueTimer.SetId(settings.TeamSignBlueTimerId)
accessPointWifiStatuses := [6]*network.TeamWifiStatus{
&arena.AllianceStations["R1"].WifiStatus,
&arena.AllianceStations["R2"].WifiStatus,
&arena.AllianceStations["R3"].WifiStatus,
&arena.AllianceStations["B1"].WifiStatus,
&arena.AllianceStations["B2"].WifiStatus,
&arena.AllianceStations["B3"].WifiStatus,
}
arena.accessPoint.SetSettings(
settings.ApAddress,
settings.ApPassword,
settings.ApChannel,
settings.NetworkSecurityEnabled,
accessPointWifiStatuses,
)
arena.networkSwitch = network.NewSwitch(settings.SwitchAddress, settings.SwitchPassword)
sccUpCommands := strings.Split(settings.SCCUpCommands, "\n")
sccDownCommands := strings.Split(settings.SCCDownCommands, "\n")
arena.redSCC = network.NewSCCSwitch(
settings.RedSCCAddress,
settings.SCCUsername,
settings.SCCPassword,
sccUpCommands,
sccDownCommands,
)
arena.blueSCC = network.NewSCCSwitch(
settings.BlueSCCAddress,
settings.SCCUsername,
settings.SCCPassword,
sccUpCommands,
sccDownCommands,
)
arena.Plc.SetAddress(settings.PlcAddress)
arena.TbaClient = partner.NewTbaClient(settings.TbaEventCode, settings.TbaSecretId, settings.TbaSecret)
arena.NexusClient = partner.NewNexusClient(settings.TbaEventCode)
arena.BlackmagicClient = partner.NewBlackmagicClient(settings.BlackmagicAddresses)
// Initialize Companion client with event configurations
companionEventConfigs := map[partner.CompanionEvent]partner.CompanionEventConfig{
partner.EventMatchPreview: {
Page: settings.CompanionMatchPreviewPage,
Row: settings.CompanionMatchPreviewRow,
Column: settings.CompanionMatchPreviewColumn,
},
partner.EventShowOverlay: {
Page: settings.CompanionSetAudiencePage,
Row: settings.CompanionSetAudienceRow,
Column: settings.CompanionSetAudienceColumn,
},
partner.EventMatchStart: {
Page: settings.CompanionMatchStartPage,
Row: settings.CompanionMatchStartRow,
Column: settings.CompanionMatchStartColumn,
},
partner.EventTeleopStart: {
Page: settings.CompanionTeleopStartPage,
Row: settings.CompanionTeleopStartRow,
Column: settings.CompanionTeleopStartColumn,
},
partner.EventEndgameStart: {
Page: settings.CompanionEndgameStartPage,
Row: settings.CompanionEndgameStartRow,
Column: settings.CompanionEndgameStartColumn,
},
partner.EventMatchEnd: {
Page: settings.CompanionMatchEndPage,
Row: settings.CompanionMatchEndRow,
Column: settings.CompanionMatchEndColumn,
},
partner.EventShowFinalScore: {
Page: settings.CompanionPostResultPage,
Row: settings.CompanionPostResultRow,
Column: settings.CompanionPostResultColumn,
},
partner.EventAllianceSelection: {
Page: settings.CompanionAllianceSelectionPage,
Row: settings.CompanionAllianceSelectionRow,
Column: settings.CompanionAllianceSelectionColumn,
},
partner.EventMatchAbort: {
Page: settings.CompanionMatchAbortPage,
Row: settings.CompanionMatchAbortRow,
Column: settings.CompanionMatchAbortColumn,
},
}
arena.CompanionClient = partner.NewCompanionClient(
settings.CompanionAddress,
settings.CompanionPort,
companionEventConfigs,
)
game.MatchTiming.WarmupDurationSec = settings.WarmupDurationSec
game.MatchTiming.AutoDurationSec = settings.AutoDurationSec
game.MatchTiming.PauseDurationSec = settings.PauseDurationSec
game.MatchTiming.TeleopDurationSec = settings.TeleopDurationSec
game.MatchTiming.WarningRemainingDurationSec = settings.WarningRemainingDurationSec
game.UpdateMatchSounds()
arena.MatchTimingNotifier.Notify()
game.AutoBonusCoralThreshold = settings.AutoBonusCoralThreshold
game.CoralBonusPerLevelThreshold = settings.CoralBonusPerLevelThreshold
game.CoralBonusCoopEnabled = settings.CoralBonusCoopEnabled
game.BargeBonusPointThreshold = settings.BargeBonusPointThreshold
game.IncludeAlgaeInBargeBonus = settings.IncludeAlgaeInBargeBonus
// Reconstruct the playoff tournament in memory.
if err = arena.CreatePlayoffTournament(); err != nil {
return err
}
if err = arena.UpdatePlayoffTournament(); err != nil {
return err
}
return nil
}
// Constructs an empty playoff tournament in memory, based only on the number of alliances.
func (arena *Arena) CreatePlayoffTournament() error {
var err error
arena.PlayoffTournament, err = playoff.NewPlayoffTournament(
arena.EventSettings.PlayoffType, arena.EventSettings.NumPlayoffAlliances,
)
return err
}
// Performs the one-time creation of all matches for the playoff tournament.
func (arena *Arena) CreatePlayoffMatches(startTime time.Time) error {
return arena.PlayoffTournament.CreateMatchesAndBreaks(arena.Database, startTime)
}
// Traverses the playoff tournament rounds to assess winners and populate subsequent matches.
func (arena *Arena) UpdatePlayoffTournament() error {
alliances, err := arena.Database.GetAllAlliances()
if err != nil {
return err
}
if len(alliances) > 0 {
return arena.PlayoffTournament.UpdateMatches(arena.Database)
}
return nil
}
// Sets up the arena for the given match.
func (arena *Arena) LoadMatch(match *model.Match) error {
if arena.MatchState != PreMatch && arena.MatchState != TimeoutActive {
return fmt.Errorf("cannot load match while there is a match still in progress or with results pending")
}
arena.CurrentMatch = match
loadedByNexus := false
if match.ShouldAllowNexusSubstitution() && arena.EventSettings.NexusEnabled {
// Attempt to get the match lineup from Nexus for FRC.
lineup, err := arena.NexusClient.GetLineup(match.TbaMatchKey)
if err != nil {
log.Printf("Failed to load lineup from Nexus: %s", err.Error())
} else {
err = arena.SubstituteTeams(lineup[0], lineup[1], lineup[2], lineup[3], lineup[4], lineup[5])
if err != nil {
log.Printf("Failed to substitute teams using Nexus lineup; loading match normally: %s", err.Error())
} else {
log.Printf(
"Successfully loaded lineup for match %s from Nexus: %v", match.TbaMatchKey.String(), *lineup,
)
loadedByNexus = true
}
}
}
if !loadedByNexus {
err := arena.assignTeam(match.Red1, "R1")
if err != nil {
return err
}
err = arena.assignTeam(match.Red2, "R2")
if err != nil {
return err
}
err = arena.assignTeam(match.Red3, "R3")
if err != nil {
return err
}
err = arena.assignTeam(match.Blue1, "B1")
if err != nil {
return err
}
err = arena.assignTeam(match.Blue2, "B2")
if err != nil {
return err
}
err = arena.assignTeam(match.Blue3, "B3")
if err != nil {
return err
}
arena.setupNetwork(
[6]*model.Team{
arena.AllianceStations["R1"].Team,
arena.AllianceStations["R2"].Team,
arena.AllianceStations["R3"].Team,
arena.AllianceStations["B1"].Team,
arena.AllianceStations["B2"].Team,
arena.AllianceStations["B3"].Team,
},
false,
)
}
// Reset the arena state and realtime scores.
arena.soundsPlayed = make(map[*game.MatchSound]struct{})
arena.RedRealtimeScore = NewRealtimeScore()
arena.BlueRealtimeScore = NewRealtimeScore()
arena.ScoringPanelRegistry.resetScoreCommitted()
arena.Plc.ResetMatch()
arena.NextFoulId = 1
// Notify any listeners about the new match.
arena.MatchLoadNotifier.Notify()
arena.RealtimeScoreNotifier.Notify()
arena.AllianceStationDisplayMode = "match"
arena.AllianceStationDisplayModeNotifier.Notify()
arena.ScoringStatusNotifier.Notify()
return nil
}
// Sets a new test match containing no teams as the current match.
func (arena *Arena) LoadTestMatch() error {
return arena.LoadMatch(&model.Match{Type: model.Test, ShortName: "T", LongName: "Test Match"})
}
// Loads the first unplayed match of the current match type.
func (arena *Arena) LoadNextMatch(startScheduledBreak bool) error {
nextMatch, err := arena.getNextMatch(false)
if err != nil {
return err
}
if nextMatch == nil {
return arena.LoadTestMatch()
}
err = arena.LoadMatch(nextMatch)
if err != nil {
return err
}
// Start the timeout timer if there is a scheduled break before this match.
if startScheduledBreak {
scheduledBreak, err := arena.Database.GetScheduledBreakByMatchTypeOrder(nextMatch.Type, nextMatch.TypeOrder)
if err != nil {
return err
}
if scheduledBreak != nil {
go func() {
time.Sleep(time.Second * scheduledBreakDelaySec)
_ = arena.StartTimeout(scheduledBreak.Description, scheduledBreak.DurationSec)
}()
}
}
return nil
}
// Assigns the given team to the given station, also substituting it into the match record.
func (arena *Arena) SubstituteTeams(red1, red2, red3, blue1, blue2, blue3 int) error {
if !arena.CurrentMatch.ShouldAllowSubstitution() {
return fmt.Errorf("Can't substitute teams for qualification matches.")
}
if err := arena.validateTeams(red1, red2, red3, blue1, blue2, blue3); err != nil {
return err
}
if err := arena.assignTeam(red1, "R1"); err != nil {
return err
}
if err := arena.assignTeam(red2, "R2"); err != nil {
return err
}
if err := arena.assignTeam(red3, "R3"); err != nil {
return err
}
if err := arena.assignTeam(blue1, "B1"); err != nil {
return err
}
if err := arena.assignTeam(blue2, "B2"); err != nil {
return err
}
if err := arena.assignTeam(blue3, "B3"); err != nil {
return err
}
arena.CurrentMatch.Red1 = red1
arena.CurrentMatch.Red2 = red2
arena.CurrentMatch.Red3 = red3
arena.CurrentMatch.Blue1 = blue1
arena.CurrentMatch.Blue2 = blue2
arena.CurrentMatch.Blue3 = blue3
arena.setupNetwork(
[6]*model.Team{
arena.AllianceStations["R1"].Team,
arena.AllianceStations["R2"].Team,
arena.AllianceStations["R3"].Team,
arena.AllianceStations["B1"].Team,
arena.AllianceStations["B2"].Team,
arena.AllianceStations["B3"].Team,
},
false,
)
arena.MatchLoadNotifier.Notify()
if arena.CurrentMatch.Type != model.Test {
arena.Database.UpdateMatch(arena.CurrentMatch)
}
return nil
}
// Starts the match if all conditions are met.
func (arena *Arena) StartMatch() error {
err := arena.checkCanStartMatch()
if err == nil {
// Save the match start time to the database for posterity.
arena.CurrentMatch.StartedAt = time.Now()
if arena.CurrentMatch.Type != model.Test {
arena.Database.UpdateMatch(arena.CurrentMatch)
}
arena.updateCycleTime(arena.CurrentMatch.StartedAt)
// Save the missed packet count to subtract it from the running count.
for _, allianceStation := range arena.AllianceStations {
if allianceStation.DsConn != nil {
err = allianceStation.DsConn.signalMatchStart(arena.CurrentMatch, &allianceStation.WifiStatus)
if err != nil {
log.Println(err)
}
}
// Save the teams that have successfully connected to the field.
if allianceStation.Team != nil && !allianceStation.Team.HasConnected && allianceStation.DsConn != nil &&
allianceStation.DsConn.RobotLinked {
allianceStation.Team.HasConnected = true
arena.Database.UpdateTeam(allianceStation.Team)
}
}
// Propagate which teams were bypassed to the tracked score.
for i := 0; i < 3; i++ {
stationNumber := strconv.Itoa(i + 1)
arena.RedRealtimeScore.CurrentScore.RobotsBypassed[i] = arena.AllianceStations["R"+stationNumber].Bypass
arena.BlueRealtimeScore.CurrentScore.RobotsBypassed[i] = arena.AllianceStations["B"+stationNumber].Bypass
}
arena.MatchState = StartMatch
}
return err
}
// Kills the current match or timeout if it is underway.
func (arena *Arena) AbortMatch() error {
if arena.MatchState == PreMatch || arena.MatchState == PostMatch || arena.MatchState == PostTimeout {
return fmt.Errorf("cannot abort match when it is not in progress")
}
if arena.MatchState == TimeoutActive {
// Handle by advancing the timeout clock to the end and letting the regular logic deal with it.
arena.MatchStartTime = time.Now().Add(-time.Second * time.Duration(game.MatchTiming.TimeoutDurationSec))
return nil
}
if arena.MatchState != WarmupPeriod {
arena.PlaySound("abort")
}
arena.MatchState = PostMatch
arena.matchAborted = true
arena.AudienceDisplayMode = "blank"
arena.AudienceDisplayModeNotifier.Notify()
go arena.BlackmagicClient.StopRecording()
go arena.CompanionClient.SendEvent(partner.EventMatchAbort)
return nil
}
// Clears out the match and resets the arena state unless there is a match underway.
func (arena *Arena) ResetMatch() error {
if arena.MatchState != PostMatch && arena.MatchState != PreMatch && arena.MatchState != TimeoutActive {
return fmt.Errorf("cannot reset match while it is in progress")
}
if arena.MatchState != TimeoutActive {
arena.MatchState = PreMatch
}
arena.matchAborted = false
arena.AllianceStations["R1"].Bypass = false
arena.AllianceStations["R2"].Bypass = false
arena.AllianceStations["R3"].Bypass = false
arena.AllianceStations["B1"].Bypass = false
arena.AllianceStations["B2"].Bypass = false
arena.AllianceStations["B3"].Bypass = false
arena.MuteMatchSounds = false
return nil
}
// Starts a timeout of the given duration.
func (arena *Arena) StartTimeout(description string, durationSec int) error {
if arena.MatchState != PreMatch {
return fmt.Errorf("cannot start timeout while there is a match still in progress or with results pending")
}
game.MatchTiming.TimeoutDurationSec = durationSec
game.UpdateMatchSounds()
arena.soundsPlayed = make(map[*game.MatchSound]struct{})
arena.MatchTimingNotifier.Notify()
arena.breakDescription = description
arena.MatchLoadNotifier.Notify()
arena.MatchState = TimeoutActive
arena.MatchStartTime = time.Now()
arena.LastMatchTimeSec = -1
arena.AllianceStationDisplayMode = "timeout"
arena.AllianceStationDisplayModeNotifier.Notify()
return nil
}
// Updates the audience display screen.
func (arena *Arena) SetAudienceDisplayMode(mode string) {
if arena.AudienceDisplayMode != mode {
arena.AudienceDisplayMode = mode
arena.AudienceDisplayModeNotifier.Notify()
if mode == "score" {
arena.PlaySound("match_result")
go arena.CompanionClient.SendEvent(partner.EventShowFinalScore)
} else if mode == "allianceSelection" {
go arena.CompanionClient.SendEvent(partner.EventAllianceSelection)
} else if mode == "intro" {
go arena.CompanionClient.SendEvent(partner.EventMatchPreview)
} else if mode == "match" {
go arena.CompanionClient.SendEvent(partner.EventShowOverlay)
}
}
}
// Updates the alliance station display screen.
func (arena *Arena) SetAllianceStationDisplayMode(mode string) {
if arena.AllianceStationDisplayMode != mode {
arena.AllianceStationDisplayMode = mode
arena.AllianceStationDisplayModeNotifier.Notify()
}
}
// Returns the fractional number of seconds since the start of the match.
func (arena *Arena) MatchTimeSec() float64 {
if arena.MatchState == PreMatch || arena.MatchState == StartMatch || arena.MatchState == PostMatch {
return 0
} else {
return time.Since(arena.MatchStartTime).Seconds()
}
}
// Performs a single iteration of checking inputs and timers and setting outputs accordingly to control the
// flow of a match.
func (arena *Arena) Update() {
// Decide what state the robots need to be in, depending on where we are in the match.
auto := false
enabled := false
sendDsPacket := false
matchTimeSec := arena.MatchTimeSec()
switch arena.MatchState {
case PreMatch:
auto = true
enabled = false
// Set all game data values to empty
for _, allianceStation := range arena.AllianceStations {
allianceStation.GameData = ""
}
case StartMatch:
arena.MatchStartTime = time.Now()
arena.LastMatchTimeSec = -1
auto = true
arena.AudienceDisplayMode = "match"
arena.AudienceDisplayModeNotifier.Notify()
arena.AllianceStationDisplayMode = "match"
arena.AllianceStationDisplayModeNotifier.Notify()
go arena.BlackmagicClient.StartRecording()
go arena.CompanionClient.SendEvent(partner.EventMatchStart)
if game.MatchTiming.WarmupDurationSec > 0 {
arena.MatchState = WarmupPeriod
enabled = false
sendDsPacket = false
} else {
arena.MatchState = AutoPeriod
enabled = true
sendDsPacket = true
}
arena.Plc.ResetMatch()
arena.FieldVolunteers = false
arena.FieldReset = false
case WarmupPeriod:
auto = true
enabled = false
if matchTimeSec >= float64(game.MatchTiming.WarmupDurationSec) {
arena.MatchState = AutoPeriod
auto = true
enabled = true
sendDsPacket = true
}
case AutoPeriod:
auto = true
enabled = true
if matchTimeSec >= game.GetDurationToAutoEnd().Seconds() {
auto = false
sendDsPacket = true
if game.MatchTiming.PauseDurationSec > 0 {
arena.MatchState = PausePeriod
enabled = false
} else {
arena.MatchState = TeleopPeriod
enabled = true
go arena.CompanionClient.SendEvent(partner.EventTeleopStart)
}
}
case PausePeriod:
auto = false
enabled = false
if matchTimeSec >= game.GetDurationToTeleopStart().Seconds() {
arena.MatchState = TeleopPeriod
auto = false
enabled = true
sendDsPacket = true
go arena.CompanionClient.SendEvent(partner.EventTeleopStart)
}
case TeleopPeriod:
auto = false
enabled = true
if matchTimeSec >= game.GetDurationToTeleopEnd().Seconds() {
arena.MatchState = PostMatch
auto = false
enabled = false
sendDsPacket = true
go arena.BlackmagicClient.StopRecording()
go arena.CompanionClient.SendEvent(partner.EventMatchEnd)
go func() {
// Leave the scores on the screen briefly at the end of the match.
time.Sleep(time.Second * matchEndScoreDwellSec)
arena.AudienceDisplayMode = "blank"
arena.AudienceDisplayModeNotifier.Notify()
}()
go func() {
// Configure the network in advance for the next match after a delay.
time.Sleep(time.Second * preLoadNextMatchDelaySec)
arena.preLoadNextMatch()
}()
}
case TimeoutActive:
if matchTimeSec >= float64(game.MatchTiming.TimeoutDurationSec) {
arena.MatchState = PostTimeout
go func() {
// Leave the timer on the screen briefly at the end of the timeout period.
time.Sleep(time.Second * matchEndScoreDwellSec)
arena.AudienceDisplayMode = "blank"
arena.AudienceDisplayModeNotifier.Notify()
arena.AllianceStationDisplayMode = "logo"
arena.AllianceStationDisplayModeNotifier.Notify()
}()
}
case PostTimeout:
if matchTimeSec >= float64(game.MatchTiming.TimeoutDurationSec+postTimeoutSec) {
arena.MatchState = PreMatch
}
}
// Send a match tick notification if passing an integer second threshold or if the match state changed.
if int(matchTimeSec) != int(arena.LastMatchTimeSec) || arena.MatchState != arena.lastMatchState {
arena.MatchTimeNotifier.Notify()
}
// Send a packet if at a period transition point or if it's been long enough since the last one.
msSinceLastDsPacket := int(time.Since(arena.lastDsPacketTime).Seconds() * 1000)
if sendDsPacket || msSinceLastDsPacket >= dsPacketPeriodMs {
if msSinceLastDsPacket >= dsPacketWarningMs && arena.lastDsPacketTime.After(time.Time{}) {
log.Printf("Warning: Long time since last driver station packet: %dms", msSinceLastDsPacket)
}
arena.sendDsPacket(auto, enabled)
arena.ArenaStatusNotifier.Notify()
}
// Handle the Companion EndGameStart event.
arena.checkEndgameStart(matchTimeSec)
arena.handleSounds(matchTimeSec)
// Handle field sensors/lights/actuators.
arena.handlePlcInputOutput()
// Handle the team number / timer displays.
arena.TeamSigns.Update(arena)
arena.LastMatchTimeSec = matchTimeSec
arena.lastMatchState = arena.MatchState
}
// Checks if the endgame warning period has started and triggers the Companion event if so.
func (arena *Arena) checkEndgameStart(matchTimeSec float64) {
// Only check during teleop period
if arena.MatchState != TeleopPeriod {
return
}
// Calculate the time when endgame warning should start
endgameStartTime := float64(
game.MatchTiming.AutoDurationSec + game.MatchTiming.PauseDurationSec +
game.MatchTiming.TeleopDurationSec - game.MatchTiming.WarningRemainingDurationSec,
)
// Check if we've crossed the endgame threshold and haven't already triggered it
if matchTimeSec >= endgameStartTime && arena.LastMatchTimeSec < endgameStartTime {
go arena.CompanionClient.SendEvent(partner.EventEndgameStart)
}
}
// Loops indefinitely to track and update the arena components.
func (arena *Arena) Run() {
// Start other loops in goroutines.
go arena.listenForDriverStations()
go arena.listenForDsUdpPackets()
go arena.accessPoint.Run()
go arena.Plc.Run()
for {
loopStartTime := time.Now()
arena.Update()
if time.Since(loopStartTime).Milliseconds() > arenaLoopWarningMs {
log.Printf("Warning: Arena loop iteration took a long time: %dms", time.Since(loopStartTime).Milliseconds())
}
if time.Since(arena.lastPeriodicTaskTime).Seconds() >= periodicTaskPeriodSec {
arena.lastPeriodicTaskTime = time.Now()
go arena.runPeriodicTasks()
}
time.Sleep(time.Millisecond * arenaLoopPeriodMs)
}
}
// Calculates the red alliance score summary for the given realtime snapshot.
func (arena *Arena) RedScoreSummary() *game.ScoreSummary {
return arena.RedRealtimeScore.CurrentScore.Summarize(&arena.BlueRealtimeScore.CurrentScore)
}
// Calculates the blue alliance score summary for the given realtime snapshot.
func (arena *Arena) BlueScoreSummary() *game.ScoreSummary {
return arena.BlueRealtimeScore.CurrentScore.Summarize(&arena.RedRealtimeScore.CurrentScore)
}
// Checks that the given teams are present in the database, allowing team ID 0 which indicates an empty spot.
func (arena *Arena) validateTeams(teamIds ...int) error {
for _, teamId := range teamIds {
if teamId == 0 {
continue
}
team, err := arena.Database.GetTeamById(teamId)
if err != nil {
return err
}
if team == nil {
return fmt.Errorf("Team %d is not present at the event.", teamId)
}
}
return nil
}
// Loads a team into an alliance station, cleaning up the previous team there if there is one.
func (arena *Arena) assignTeam(teamId int, station string) error {
// Reject invalid station values.
if _, ok := arena.AllianceStations[station]; !ok {
return fmt.Errorf("Invalid alliance station '%s'.", station)
}
// Force the A-stop to be reset by the new team if it is already pressed (if the PLC is enabled).
arena.AllianceStations[station].aStopReset = !arena.Plc.IsEnabled()
// Do nothing if the station is already assigned to the requested team.
dsConn := arena.AllianceStations[station].DsConn
if dsConn != nil && dsConn.TeamId == teamId {
return nil
}
if dsConn != nil {
dsConn.close()
arena.AllianceStations[station].Team = nil
arena.AllianceStations[station].DsConn = nil
}
// Leave the station empty if the team number is zero.
if teamId == 0 {
arena.AllianceStations[station].Team = nil
return nil
}
// Load the team model. If it doesn't exist, enable anonymous operation.
team, err := arena.Database.GetTeamById(teamId)
if err != nil {
return err
}
if team == nil {
team = &model.Team{Id: teamId}
}
arena.AllianceStations[station].Team = team
return nil
}
// Returns the next match of the same type that is currently loaded, or nil if there are no more matches.
func (arena *Arena) getNextMatch(excludeCurrent bool) (*model.Match, error) {
if arena.CurrentMatch.Type == model.Test {
return nil, nil
}
matches, err := arena.Database.GetMatchesByType(arena.CurrentMatch.Type, false)
if err != nil {
return nil, err
}
for _, match := range matches {
if !match.IsComplete() && !(excludeCurrent && match.Id == arena.CurrentMatch.Id) {
return &match, nil
}
}
// There are no matches left of the same type.
return nil, nil
}
// Configures the field network for the next match in advance of the current match being scored and committed.
func (arena *Arena) preLoadNextMatch() {
if arena.MatchState != PostMatch {
// The next match has already been loaded; no need to do anything.
return
}
nextMatch, err := arena.getNextMatch(true)
if err != nil {
log.Printf("Failed to pre-load next match: %s", err.Error())
}
if nextMatch == nil {
return
}
teamIds := [6]int{nextMatch.Red1, nextMatch.Red2, nextMatch.Red3, nextMatch.Blue1, nextMatch.Blue2, nextMatch.Blue3}
if nextMatch.ShouldAllowNexusSubstitution() && arena.EventSettings.NexusEnabled {
// Attempt to get the match lineup from Nexus for FRC.
lineup, err := arena.NexusClient.GetLineup(nextMatch.TbaMatchKey)
if err != nil {
log.Printf("Failed to load lineup from Nexus: %s", err.Error())
} else {
teamIds = *lineup
}
}
var teams [6]*model.Team
for i, teamId := range teamIds {
if teamId == 0 {
continue
}
if teams[i], err = arena.Database.GetTeamById(teamId); err != nil {
log.Printf("Failed to get model for Team %d while pre-loading next match: %s", teamId, err.Error())
}
}
arena.setupNetwork(teams, true)
arena.TeamSigns.SetNextMatchTeams(teamIds)
}
// Enable or disable the team ethernet ports on both SCCs
func (arena *Arena) setSCCEthernetEnabled(enabled bool) {
if arena.EventSettings.SCCManagementEnabled {
var wg sync.WaitGroup
wg.Add(2)
configureSCC := func(scc *network.SCCSwitch, name string) {
defer wg.Done()
err := scc.SetTeamEthernetEnabled(enabled)
if err != nil {
log.Printf("Failed to set %s SCC enabled state to %t: %s", name, enabled, err.Error())
}
}
go configureSCC(arena.redSCC, "red")
go configureSCC(arena.blueSCC, "blue")
wg.Wait()
}
}
// Asynchronously reconfigures the networking hardware for the new set of teams.
func (arena *Arena) setupNetwork(teams [6]*model.Team, isPreload bool) {
if isPreload {
arena.preloadedTeams = &teams
} else if arena.preloadedTeams != nil {
preloadedTeams := *arena.preloadedTeams
arena.preloadedTeams = nil
if reflect.DeepEqual(teams, preloadedTeams) {
// Skip configuring the network; this is the same set of teams that was preloaded.
return
}
}
if arena.EventSettings.NetworkSecurityEnabled {
if err := arena.accessPoint.ConfigureTeamWifi(teams); err != nil {
log.Printf("Failed to configure team WiFi: %s", err.Error())
}
go func() {
arena.setSCCEthernetEnabled(false)
if err := arena.networkSwitch.ConfigureTeamEthernet(teams); err != nil {
log.Printf("Failed to configure team Ethernet: %s", err.Error())
}
arena.setSCCEthernetEnabled(true)
}()
}
}
// Returns nil if the match can be started, and an error otherwise.
func (arena *Arena) checkCanStartMatch() error {
if arena.MatchState != PreMatch {
return fmt.Errorf("cannot start match while there is a match still in progress or with results pending")
}
err := arena.checkAllianceStationsReady("R1", "R2", "R3", "B1", "B2", "B3")
if err != nil {
return err
}
if arena.Plc.IsEnabled() {
if !arena.Plc.IsHealthy() {
return fmt.Errorf("cannot start match while PLC is not healthy")
}
if arena.Plc.GetFieldEStop() {
return fmt.Errorf("cannot start match while field emergency stop is active")
}
for name, status := range arena.Plc.GetArmorBlockStatuses() {
if !status {
return fmt.Errorf("cannot start match while PLC ArmorBlock %q is not connected", name)
}
}
}
return nil
}
func (arena *Arena) checkAllianceStationsReady(stations ...string) error {
for _, station := range stations {
allianceStation := arena.AllianceStations[station]
if allianceStation.EStop {
return fmt.Errorf("cannot start match while an emergency stop is active")
}
if !allianceStation.aStopReset {
return fmt.Errorf("cannot start match if an autonomous stop has not been reset since the previous match")
}
if !allianceStation.Bypass {
if allianceStation.DsConn == nil || !allianceStation.DsConn.RobotLinked {
return fmt.Errorf("cannot start match until all robots are connected or bypassed")
}
}
}
return nil
}
func (arena *Arena) sendDsPacket(auto bool, enabled bool) {
for _, allianceStation := range arena.AllianceStations {
dsConn := allianceStation.DsConn
if dsConn != nil {
dsConn.Auto = auto
dsConn.Enabled = enabled && !allianceStation.EStop && !(auto && allianceStation.AStop) &&
!allianceStation.Bypass
dsConn.EStop = allianceStation.EStop
dsConn.AStop = allianceStation.AStop
err := dsConn.update(arena, allianceStation.GameData)
if err != nil {
log.Printf("Unable to send driver station packet for team %d.", allianceStation.Team.Id)
}
}
}
arena.lastDsPacketTime = time.Now()
}
// Returns the alliance station identifier for the given team, or the empty string if the team is not present
// in the current match.
func (arena *Arena) getAssignedAllianceStation(teamId int) string {
for station, allianceStation := range arena.AllianceStations {
if allianceStation.Team != nil && allianceStation.Team.Id == teamId {
return station
}
}
return ""
}
// Updates the score given new input information from the field PLC, and actuates PLC outputs accordingly.
func (arena *Arena) handlePlcInputOutput() {
if !arena.Plc.IsEnabled() {
return
}
// Handle PLC functions that are always active.
if arena.Plc.GetFieldEStop() && !arena.matchAborted {
arena.AbortMatch()
}
redEStops, blueEStops := arena.Plc.GetTeamEStops()
redAStops, blueAStops := arena.Plc.GetTeamAStops()
arena.handleTeamStop("R1", redEStops[0], redAStops[0])
arena.handleTeamStop("R2", redEStops[1], redAStops[1])
arena.handleTeamStop("R3", redEStops[2], redAStops[2])
arena.handleTeamStop("B1", blueEStops[0], blueAStops[0])
arena.handleTeamStop("B2", blueEStops[1], blueAStops[1])
arena.handleTeamStop("B3", blueEStops[2], blueAStops[2])
redEthernets, blueEthernets := arena.Plc.GetEthernetConnected()
arena.AllianceStations["R1"].Ethernet = redEthernets[0]
arena.AllianceStations["R2"].Ethernet = redEthernets[1]
arena.AllianceStations["R3"].Ethernet = redEthernets[2]
arena.AllianceStations["B1"].Ethernet = blueEthernets[0]
arena.AllianceStations["B2"].Ethernet = blueEthernets[1]
arena.AllianceStations["B3"].Ethernet = blueEthernets[2]
// Handle in-match PLC functions.
redScore := &arena.RedRealtimeScore.CurrentScore
oldRedScore := *redScore
blueScore := &arena.BlueRealtimeScore.CurrentScore
oldBlueScore := *blueScore
matchStartTime := arena.MatchStartTime
currentTime := time.Now()
teleopGracePeriod := matchStartTime.Add(game.GetDurationToTeleopEnd() + game.TeleopGracePeriodSec*time.Second)
inGracePeriod := arena.MatchState == PostMatch && currentTime.Before(teleopGracePeriod) && !arena.matchAborted
redAllianceReady := arena.checkAllianceStationsReady("R1", "R2", "R3") == nil
blueAllianceReady := arena.checkAllianceStationsReady("B1", "B2", "B3") == nil
// Handle the evergreen PLC functions: stack lights, stack buzzer, and field reset light.
switch arena.MatchState {
case PreMatch:
if arena.lastMatchState != PreMatch {
arena.Plc.SetFieldResetLight(true)
}
fallthrough
case TimeoutActive:
fallthrough
case PostTimeout:
// Set the stack light state -- solid alliance color(s) if robots are not connected, solid orange if scores are
// not input, or blinking green if ready.
greenStackLight := redAllianceReady && blueAllianceReady && arena.Plc.GetCycleState(2, 0, 2)
arena.Plc.SetStackLights(!redAllianceReady, !blueAllianceReady, false, greenStackLight)
arena.Plc.SetStackBuzzer(redAllianceReady && blueAllianceReady)
// Turn off lights if all teams become ready.
if redAllianceReady && blueAllianceReady {
arena.FieldVolunteers = false
arena.FieldReset = false
arena.Plc.SetFieldResetLight(false)
if arena.CurrentMatch.FieldReadyAt.IsZero() {
arena.CurrentMatch.FieldReadyAt = time.Now()
}
}
case PostMatch:
if arena.FieldReset {
arena.Plc.SetFieldResetLight(true)
}
scoreReady := arena.RedRealtimeScore.FoulsCommitted && arena.BlueRealtimeScore.FoulsCommitted &&
arena.positionPostMatchScoreReady("red_near") && arena.positionPostMatchScoreReady("red_far") &&
arena.positionPostMatchScoreReady("blue_near") && arena.positionPostMatchScoreReady("blue_far")
arena.Plc.SetStackLights(false, false, !scoreReady, false)
case AutoPeriod, PausePeriod, TeleopPeriod:
arena.Plc.SetStackBuzzer(false)
arena.Plc.SetStackLights(!redAllianceReady, !blueAllianceReady, false, true)
}
// Get all the game-specific inputs and update the score.
if arena.MatchState == AutoPeriod || arena.MatchState == PausePeriod || arena.MatchState == TeleopPeriod ||
inGracePeriod {
redScore.ProcessorAlgae, blueScore.ProcessorAlgae = arena.Plc.GetProcessorCounts()
}
if !oldRedScore.Equals(redScore) || !oldBlueScore.Equals(blueScore) {
arena.RealtimeScoreNotifier.Notify()
}
// Handle the truss lights.
if arena.MatchState == AutoPeriod || arena.MatchState == PausePeriod || arena.MatchState == TeleopPeriod {
warningSequenceActive, lights := trussLightWarningSequence(arena.MatchTimeSec())
if warningSequenceActive {
arena.Plc.SetTrussLights(lights, lights)
} else {
if !game.CoralBonusCoopEnabled || arena.CurrentMatch.Type == model.Playoff {
// Just leave the lights on all match if co-op is not enabled for this match (or event).
arena.Plc.SetTrussLights([3]bool{true, true, true}, [3]bool{true, true, true})
} else {
// Set the lights to reflect co-op status.
if arena.RedScoreSummary().CoopertitionBonus && arena.BlueScoreSummary().CoopertitionBonus {
arena.Plc.SetTrussLights([3]bool{true, true, true}, [3]bool{true, true, true})
} else {
arena.Plc.SetTrussLights(
[3]bool{
arena.RedRealtimeScore.CurrentScore.ProcessorAlgae >= 1,
arena.RedRealtimeScore.CurrentScore.ProcessorAlgae >= 2,
false,
},
[3]bool{
arena.BlueRealtimeScore.CurrentScore.ProcessorAlgae >= 1,
arena.BlueRealtimeScore.CurrentScore.ProcessorAlgae >= 2,
false,
},
)
}
}
}
} else {
arena.Plc.SetTrussLights(
[3]bool{inGracePeriod, inGracePeriod, inGracePeriod}, [3]bool{inGracePeriod, inGracePeriod, inGracePeriod},
)
}
}
func (arena *Arena) handleTeamStop(station string, eStopState, aStopState bool) {
allianceStation := arena.AllianceStations[station]
if eStopState {
allianceStation.EStop = true
} else if arena.MatchTimeSec() == 0 {
// Keep the E-stop latched until the match is over.
allianceStation.EStop = false
}
if aStopState {
allianceStation.AStop = true
} else if arena.MatchState != AutoPeriod {
// Keep the A-stop latched until the autonomous period is over.
allianceStation.AStop = false
allianceStation.aStopReset = true
}
}
func (arena *Arena) handleSounds(matchTimeSec float64) {
if arena.MatchState == PreMatch || arena.MatchState == TimeoutActive || arena.MatchState == PostTimeout {
// Only apply this logic during a match.
return
}
for _, sound := range game.MatchSounds {
if sound.MatchTimeSec < 0 {
// Skip sounds with negative timestamps; they are meant to only be triggered explicitly.
continue
}
if _, ok := arena.soundsPlayed[sound]; !ok {
if matchTimeSec >= sound.MatchTimeSec && matchTimeSec-sound.MatchTimeSec < 1 {
arena.PlaySound(sound.Name)
arena.soundsPlayed[sound] = struct{}{}
}
}
}
}
func (arena *Arena) PlaySound(name string) {
if !arena.MuteMatchSounds {
arena.PlaySoundNotifier.NotifyWithMessage(name)
}
}
func (arena *Arena) positionPostMatchScoreReady(position string) bool {
numPanels := arena.ScoringPanelRegistry.GetNumPanels(position)
return numPanels > 0 && arena.ScoringPanelRegistry.GetNumScoreCommitted(position) >= numPanels
}
// Performs any actions that need to run at the interval specified by periodicTaskPeriodSec.
func (arena *Arena) runPeriodicTasks() {
arena.updateEarlyLateMessage()
arena.purgeDisconnectedDisplays()
}
// trussLightWarningSequence generates the sequence of truss light states during the "sonar ping" warning sound. It
// returns true if the sequence is active, and an array of booleans indicating the state of each truss light.
func trussLightWarningSequence(matchTimeSec float64) (bool, [3]bool) {
stepTimeSec := 0.2
sequence := []int{1, 2, 3, 2, 1, 2, 3, 0, 0, 1, 2, 3, 2, 1, 2, 3, 0, 0}
startTime := float64(
game.MatchTiming.WarmupDurationSec + game.MatchTiming.AutoDurationSec + game.MatchTiming.PauseDurationSec +
game.MatchTiming.TeleopDurationSec - game.MatchTiming.WarningRemainingDurationSec,
)
lights := [3]bool{false, false, false}
if matchTimeSec < startTime {
// The sequence is not active yet.
return false, lights
}
step := int((matchTimeSec - startTime) / stepTimeSec)
if step < len(sequence) && sequence[step] > 0 {
lights[sequence[step]-1] = true
}
return step < len(sequence), lights
}
================================================
FILE: field/arena_notifiers.go
================================================
// Copyright 2018 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Contains configuration of the publish-subscribe notifiers that allow the arena to push updates to websocket clients.
package field
import (
"github.com/Team254/cheesy-arena/game"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/playoff"
"github.com/Team254/cheesy-arena/websocket"
"strconv"
)
type ArenaNotifiers struct {
AllianceSelectionNotifier *websocket.Notifier
AllianceStationDisplayModeNotifier *websocket.Notifier
ArenaStatusNotifier *websocket.Notifier
AudienceDisplayModeNotifier *websocket.Notifier
DisplayConfigurationNotifier *websocket.Notifier
EventStatusNotifier *websocket.Notifier
LowerThirdNotifier *websocket.Notifier
MatchLoadNotifier *websocket.Notifier
MatchTimeNotifier *websocket.Notifier
MatchTimingNotifier *websocket.Notifier
PlaySoundNotifier *websocket.Notifier
RealtimeScoreNotifier *websocket.Notifier
ReloadDisplaysNotifier *websocket.Notifier
ScorePostedNotifier *websocket.Notifier
ScoringStatusNotifier *websocket.Notifier
}
type MatchTimeMessage struct {
MatchState
MatchTimeSec int
}
type audienceAllianceScoreFields struct {
Score *game.Score
ScoreSummary *game.ScoreSummary
}
// Instantiates notifiers and configures their message producing methods.
func (arena *Arena) configureNotifiers() {
arena.AllianceSelectionNotifier = websocket.NewNotifier("allianceSelection", arena.generateAllianceSelectionMessage)
arena.AllianceStationDisplayModeNotifier = websocket.NewNotifier(
"allianceStationDisplayMode", arena.generateAllianceStationDisplayModeMessage,
)
arena.ArenaStatusNotifier = websocket.NewNotifier("arenaStatus", arena.generateArenaStatusMessage)
arena.AudienceDisplayModeNotifier = websocket.NewNotifier(
"audienceDisplayMode", arena.generateAudienceDisplayModeMessage,
)
arena.DisplayConfigurationNotifier = websocket.NewNotifier(
"displayConfiguration", arena.generateDisplayConfigurationMessage,
)
arena.EventStatusNotifier = websocket.NewNotifier("eventStatus", arena.generateEventStatusMessage)
arena.LowerThirdNotifier = websocket.NewNotifier("lowerThird", arena.generateLowerThirdMessage)
arena.MatchLoadNotifier = websocket.NewNotifier("matchLoad", arena.GenerateMatchLoadMessage)
arena.MatchTimeNotifier = websocket.NewNotifier("matchTime", arena.generateMatchTimeMessage)
arena.MatchTimingNotifier = websocket.NewNotifier("matchTiming", arena.generateMatchTimingMessage)
arena.PlaySoundNotifier = websocket.NewNotifier("playSound", nil)
arena.RealtimeScoreNotifier = websocket.NewNotifier("realtimeScore", arena.generateRealtimeScoreMessage)
arena.ReloadDisplaysNotifier = websocket.NewNotifier("reload", nil)
arena.ScorePostedNotifier = websocket.NewNotifier("scorePosted", arena.GenerateScorePostedMessage)
arena.ScoringStatusNotifier = websocket.NewNotifier("scoringStatus", arena.generateScoringStatusMessage)
}
func (arena *Arena) generateAllianceSelectionMessage() any {
return &struct {
Alliances []model.Alliance
ShowTimer bool
TimeRemainingSec int
RankedTeams []model.AllianceSelectionRankedTeam
}{
arena.AllianceSelectionAlliances,
arena.AllianceSelectionShowTimer,
arena.AllianceSelectionTimeRemainingSec,
arena.AllianceSelectionRankedTeams,
}
}
func (arena *Arena) generateAllianceStationDisplayModeMessage() any {
return arena.AllianceStationDisplayMode
}
func (arena *Arena) generateArenaStatusMessage() any {
return &struct {
MatchId int
AllianceStations map[string]*AllianceStation
MatchState
CanStartMatch bool
AccessPointStatus string
SwitchStatus string
RedSCCStatus string
BlueSCCStatus string
PlcIsHealthy bool
FieldEStop bool
PlcArmorBlockStatuses map[string]bool
}{
arena.CurrentMatch.Id,
arena.AllianceStations,
arena.MatchState,
arena.checkCanStartMatch() == nil,
arena.accessPoint.Status,
arena.networkSwitch.Status,
arena.redSCC.Status,
arena.blueSCC.Status,
arena.Plc.IsHealthy(),
arena.Plc.GetFieldEStop(),
arena.Plc.GetArmorBlockStatuses(),
}
}
func (arena *Arena) generateAudienceDisplayModeMessage() any {
return arena.AudienceDisplayMode
}
func (arena *Arena) generateDisplayConfigurationMessage() any {
// Notify() for this notifier must always called from a method that has a lock on the display mutex.
// Make a copy of the map to avoid potential data races; otherwise the same map would get iterated through as it is
// serialized to JSON, outside the mutex lock.
displaysCopy := make(map[string]Display)
for displayId, display := range arena.Displays {
displaysCopy[displayId] = *display
}
return displaysCopy
}
func (arena *Arena) generateEventStatusMessage() any {
return arena.EventStatus
}
func (arena *Arena) generateLowerThirdMessage() any {
return &struct {
LowerThird *model.LowerThird
ShowLowerThird bool
}{arena.LowerThird, arena.ShowLowerThird}
}
func (arena *Arena) GenerateMatchLoadMessage() any {
teams := make(map[string]*model.Team)
var allTeamIds []int
for station, allianceStation := range arena.AllianceStations {
teams[station] = allianceStation.Team
if allianceStation.Team != nil {
allTeamIds = append(allTeamIds, allianceStation.Team.Id)
}
}
matchResult, _ := arena.Database.GetMatchResultForMatch(arena.CurrentMatch.Id)
isReplay := matchResult != nil
var matchup *playoff.Matchup
redOffFieldTeams := []*model.Team{}
blueOffFieldTeams := []*model.Team{}
if arena.CurrentMatch.Type == model.Playoff {
matchGroup := arena.PlayoffTournament.MatchGroups()[arena.CurrentMatch.PlayoffMatchGroupId]
matchup, _ = matchGroup.(*playoff.Matchup)
redOffFieldTeamIds, blueOffFieldTeamIds, _ := arena.Database.GetOffFieldTeamIds(arena.CurrentMatch)
for _, teamId := range redOffFieldTeamIds {
team, _ := arena.Database.GetTeamById(teamId)
redOffFieldTeams = append(redOffFieldTeams, team)
allTeamIds = append(allTeamIds, teamId)
}
for _, teamId := range blueOffFieldTeamIds {
team, _ := arena.Database.GetTeamById(teamId)
blueOffFieldTeams = append(blueOffFieldTeams, team)
allTeamIds = append(allTeamIds, teamId)
}
}
rankings := make(map[string]int)
for _, teamId := range allTeamIds {
ranking, _ := arena.Database.GetRankingForTeam(teamId)
if ranking != nil {
rankings[strconv.Itoa(teamId)] = ranking.Rank
}
}
return &struct {
Match *model.Match
AllowSubstitution bool
IsReplay bool
Teams map[string]*model.Team
Rankings map[string]int
Matchup *playoff.Matchup
RedOffFieldTeams []*model.Team
BlueOffFieldTeams []*model.Team
BreakDescription string
}{
arena.CurrentMatch,
arena.CurrentMatch.ShouldAllowSubstitution(),
isReplay,
teams,
rankings,
matchup,
redOffFieldTeams,
blueOffFieldTeams,
arena.breakDescription,
}
}
func (arena *Arena) generateMatchTimeMessage() any {
return MatchTimeMessage{arena.MatchState, int(arena.MatchTimeSec())}
}
func (arena *Arena) generateMatchTimingMessage() any {
return &game.MatchTiming
}
func (arena *Arena) generateRealtimeScoreMessage() any {
fields := struct {
Red *audienceAllianceScoreFields
Blue *audienceAllianceScoreFields
RedCards map[string]string
BlueCards map[string]string
MatchState
}{
getAudienceAllianceScoreFields(arena.RedRealtimeScore, arena.RedScoreSummary()),
getAudienceAllianceScoreFields(arena.BlueRealtimeScore, arena.BlueScoreSummary()),
arena.RedRealtimeScore.Cards,
arena.BlueRealtimeScore.Cards,
arena.MatchState,
}
return &fields
}
func (arena *Arena) GenerateScorePostedMessage() any {
redScoreSummary := arena.SavedMatchResult.RedScoreSummary()
blueScoreSummary := arena.SavedMatchResult.BlueScoreSummary()
redRankingPoints := redScoreSummary.BonusRankingPoints
blueRankingPoints := blueScoreSummary.BonusRankingPoints
switch arena.SavedMatch.Status {
case game.RedWonMatch:
redRankingPoints += 3
case game.BlueWonMatch:
blueRankingPoints += 3
case game.TieMatch:
redRankingPoints++
blueRankingPoints++
}
// For playoff matches, summarize the state of the series.
var redWins, blueWins int
var redDestination, blueDestination string
redOffFieldTeamIds := []int{}
blueOffFieldTeamIds := []int{}
if arena.SavedMatch.Type == model.Playoff {
matchGroup := arena.PlayoffTournament.MatchGroups()[arena.SavedMatch.PlayoffMatchGroupId]
if matchup, ok := matchGroup.(*playoff.Matchup); ok {
redWins = matchup.RedAllianceWins
blueWins = matchup.BlueAllianceWins
redDestination = matchup.RedAllianceDestination()
blueDestination = matchup.BlueAllianceDestination()
}
redOffFieldTeamIds, blueOffFieldTeamIds, _ = arena.Database.GetOffFieldTeamIds(arena.SavedMatch)
}
redRankings := map[int]*game.Ranking{
arena.SavedMatch.Red1: nil, arena.SavedMatch.Red2: nil, arena.SavedMatch.Red3: nil,
}
blueRankings := map[int]*game.Ranking{
arena.SavedMatch.Blue1: nil, arena.SavedMatch.Blue2: nil, arena.SavedMatch.Blue3: nil,
}
for index, ranking := range arena.SavedRankings {
if _, ok := redRankings[ranking.TeamId]; ok {
redRankings[ranking.TeamId] = &arena.SavedRankings[index]
}
if _, ok := blueRankings[ranking.TeamId]; ok {
blueRankings[ranking.TeamId] = &arena.SavedRankings[index]
}
}
return &struct {
Match *model.Match
RedScoreSummary *game.ScoreSummary
BlueScoreSummary *game.ScoreSummary
RedRankingPoints int
BlueRankingPoints int
RedFouls []game.Foul
BlueFouls []game.Foul
RulesViolated map[int]*game.Rule
RedCards map[string]string
BlueCards map[string]string
RedRankings map[int]*game.Ranking
BlueRankings map[int]*game.Ranking
RedOffFieldTeamIds []int
BlueOffFieldTeamIds []int
RedWon bool
BlueWon bool
RedWins int
BlueWins int
RedDestination string
BlueDestination string
CoopertitionEnabled bool
}{
arena.SavedMatch,
redScoreSummary,
blueScoreSummary,
redRankingPoints,
blueRankingPoints,
arena.SavedMatchResult.RedScore.Fouls,
arena.SavedMatchResult.BlueScore.Fouls,
getRulesViolated(arena.SavedMatchResult.RedScore.Fouls, arena.SavedMatchResult.BlueScore.Fouls),
arena.SavedMatchResult.RedCards,
arena.SavedMatchResult.BlueCards,
redRankings,
blueRankings,
redOffFieldTeamIds,
blueOffFieldTeamIds,
arena.SavedMatch.Status == game.RedWonMatch,
arena.SavedMatch.Status == game.BlueWonMatch,
redWins,
blueWins,
redDestination,
blueDestination,
game.CoralBonusCoopEnabled,
}
}
func (arena *Arena) generateScoringStatusMessage() any {
type positionStatus struct {
Ready bool
NumPanels int
NumPanelsReady int
}
getStatusForPosition := func(position string) positionStatus {
return positionStatus{
Ready: arena.positionPostMatchScoreReady(position),
NumPanels: arena.ScoringPanelRegistry.GetNumPanels(position),
NumPanelsReady: arena.GetNumScoreCommitted(position),
}
}
return &struct {
RefereeScoreReady bool
PositionStatuses map[string]positionStatus
}{
arena.RedRealtimeScore.FoulsCommitted && arena.BlueRealtimeScore.FoulsCommitted,
map[string]positionStatus{
"red_near": getStatusForPosition("red_near"),
"red_far": getStatusForPosition("red_far"),
"blue_near": getStatusForPosition("blue_near"),
"blue_far": getStatusForPosition("blue_far"),
},
}
}
// Constructs the data object for one alliance sent to the audience display for the realtime scoring overlay.
func getAudienceAllianceScoreFields(
allianceScore *RealtimeScore,
allianceScoreSummary *game.ScoreSummary,
) *audienceAllianceScoreFields {
fields := new(audienceAllianceScoreFields)
fields.Score = &allianceScore.CurrentScore
fields.ScoreSummary = allianceScoreSummary
return fields
}
// Produce a map of rules that were violated by either alliance so that they are available to the announcer.
func getRulesViolated(redFouls, blueFouls []game.Foul) map[int]*game.Rule {
rules := make(map[int]*game.Rule)
for _, foul := range redFouls {
rules[foul.RuleId] = game.GetRuleById(foul.RuleId)
}
for _, foul := range blueFouls {
rules[foul.RuleId] = game.GetRuleById(foul.RuleId)
}
return rules
}
================================================
FILE: field/arena_test.go
================================================
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package field
import (
"github.com/Team254/cheesy-arena/game"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/partner"
"github.com/Team254/cheesy-arena/playoff"
"github.com/Team254/cheesy-arena/tournament"
"github.com/Team254/cheesy-arena/websocket"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
)
func TestAssignTeam(t *testing.T) {
arena := setupTestArena(t)
team := model.Team{Id: 254}
err := arena.Database.CreateTeam(&team)
assert.Nil(t, err)
err = arena.Database.CreateTeam(&model.Team{Id: 1114})
assert.Nil(t, err)
err = arena.assignTeam(254, "B1")
assert.Nil(t, err)
assert.Equal(t, team, *arena.AllianceStations["B1"].Team)
dummyDs := &DriverStationConnection{TeamId: 254}
arena.AllianceStations["B1"].DsConn = dummyDs
// Nothing should happen if the same team is assigned to the same station.
err = arena.assignTeam(254, "B1")
assert.Nil(t, err)
assert.Equal(t, team, *arena.AllianceStations["B1"].Team)
assert.NotNil(t, arena.AllianceStations["B1"])
assert.Equal(t, dummyDs, arena.AllianceStations["B1"].DsConn) // Pointer equality
// Test reassignment to another team.
err = arena.assignTeam(1114, "B1")
assert.Nil(t, err)
assert.NotEqual(t, team, *arena.AllianceStations["B1"].Team)
assert.Nil(t, arena.AllianceStations["B1"].DsConn)
// Check assigning zero as the team number.
err = arena.assignTeam(0, "R2")
assert.Nil(t, err)
assert.Nil(t, arena.AllianceStations["R2"].Team)
assert.Nil(t, arena.AllianceStations["R2"].DsConn)
// Check assigning to a non-existent station.
err = arena.assignTeam(254, "R4")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Invalid alliance station")
}
}
func TestArenaCheckCanStartMatch(t *testing.T) {
arena := setupTestArena(t)
// Check robot state constraints.
err := arena.checkCanStartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot start match until all robots are connected or bypassed")
}
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].Bypass = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
err = arena.checkCanStartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot start match until all robots are connected or bypassed")
}
arena.AllianceStations["B3"].Bypass = true
assert.Nil(t, arena.checkCanStartMatch())
// Check PLC constraints.
arena.Plc.SetAddress("1.2.3.4")
err = arena.checkCanStartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot start match while PLC is not healthy")
}
arena.Plc.SetAddress("")
assert.Nil(t, arena.checkCanStartMatch())
}
func TestArenaMatchFlow(t *testing.T) {
arena := setupTestArena(t)
arena.Database.CreateTeam(&model.Team{Id: 254})
assert.Nil(t, arena.assignTeam(254, "B3"))
dummyDs := &DriverStationConnection{TeamId: 254}
arena.AllianceStations["B3"].DsConn = dummyDs
arena.Database.CreateTeam(&model.Team{Id: 1678})
assert.Nil(t, arena.assignTeam(254, "R2"))
dummyDs = &DriverStationConnection{TeamId: 1678}
arena.AllianceStations["R2"].DsConn = dummyDs
// Check pre-match state and packet timing.
assert.Equal(t, PreMatch, arena.MatchState)
arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond)
arena.Update()
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
lastPacketCount := arena.AllianceStations["B3"].DsConn.packetCount
arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-10 * time.Millisecond)
arena.Update()
assert.Equal(t, lastPacketCount, arena.AllianceStations["B3"].DsConn.packetCount)
arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond)
arena.Update()
assert.Equal(t, lastPacketCount+1, arena.AllianceStations["B3"].DsConn.packetCount)
// Check match start, autonomous and transition to teleop.
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].DsConn.RobotLinked = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B3"].DsConn.RobotLinked = true
assert.Nil(t, arena.StartMatch())
arena.Update()
assert.Equal(t, WarmupPeriod, arena.MatchState)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
assert.Equal(t, true, arena.RedRealtimeScore.CurrentScore.RobotsBypassed[0])
assert.Equal(t, false, arena.RedRealtimeScore.CurrentScore.RobotsBypassed[1])
assert.Equal(t, true, arena.RedRealtimeScore.CurrentScore.RobotsBypassed[2])
assert.Equal(t, true, arena.BlueRealtimeScore.CurrentScore.RobotsBypassed[0])
assert.Equal(t, true, arena.BlueRealtimeScore.CurrentScore.RobotsBypassed[1])
assert.Equal(t, false, arena.BlueRealtimeScore.CurrentScore.RobotsBypassed[2])
arena.Update()
assert.Equal(t, WarmupPeriod, arena.MatchState)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
arena.Update()
assert.Equal(t, AutoPeriod, arena.MatchState)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled)
arena.Update()
assert.Equal(t, AutoPeriod, arena.MatchState)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled)
arena.MatchStartTime = time.Now().Add(
-time.Duration(game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec) * time.Second,
)
arena.Update()
assert.Equal(t, PausePeriod, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
arena.Update()
assert.Equal(t, PausePeriod, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
arena.MatchStartTime = time.Now().Add(
-time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec,
) * time.Second,
)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled)
// Check E-stop and bypass.
arena.AllianceStations["B3"].EStop = true
arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
arena.AllianceStations["B3"].Bypass = true
arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
arena.AllianceStations["B3"].EStop = false
arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
arena.AllianceStations["B3"].Bypass = false
arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Enabled)
// Check match end.
arena.MatchStartTime = time.Now().Add(
-time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+
game.MatchTiming.TeleopDurationSec,
) * time.Second,
)
arena.Update()
assert.Equal(t, PostMatch, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
arena.Update()
assert.Equal(t, PostMatch, arena.MatchState)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
arena.AllianceStations["R1"].Bypass = true
arena.ResetMatch()
arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond)
arena.Update()
assert.Equal(t, PreMatch, arena.MatchState)
assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled)
assert.Equal(t, false, arena.AllianceStations["R1"].Bypass)
}
func TestArenaStateEnforcement(t *testing.T) {
arena := setupTestArena(t)
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].Bypass = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B3"].Bypass = true
err := arena.LoadMatch(new(model.Match))
assert.Nil(t, err)
err = arena.AbortMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot abort match when")
}
err = arena.StartMatch()
assert.Nil(t, err)
err = arena.LoadMatch(new(model.Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot load match while")
}
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot start match while")
}
err = arena.ResetMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot reset match while")
}
arena.MatchState = AutoPeriod
err = arena.LoadMatch(new(model.Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot load match while")
}
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot start match while")
}
err = arena.ResetMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot reset match while")
}
arena.MatchState = PausePeriod
err = arena.LoadMatch(new(model.Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot load match while")
}
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot start match while")
}
err = arena.ResetMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot reset match while")
}
arena.MatchState = TeleopPeriod
err = arena.LoadMatch(new(model.Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot load match while")
}
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot start match while")
}
err = arena.ResetMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot reset match while")
}
arena.MatchState = PostMatch
err = arena.LoadMatch(new(model.Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot load match while")
}
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot start match while")
}
err = arena.AbortMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "cannot abort match when")
}
err = arena.ResetMatch()
assert.Nil(t, err)
assert.Equal(t, PreMatch, arena.MatchState)
err = arena.ResetMatch()
assert.Nil(t, err)
err = arena.LoadMatch(new(model.Match))
assert.Nil(t, err)
}
func TestMatchStartRobotLinkEnforcement(t *testing.T) {
arena := setupTestArena(t)
arena.Database.CreateTeam(&model.Team{Id: 101})
arena.Database.CreateTeam(&model.Team{Id: 102})
arena.Database.CreateTeam(&model.Team{Id: 103})
arena.Database.CreateTeam(&model.Team{Id: 104})
arena.Database.CreateTeam(&model.Team{Id: 105})
arena.Database.CreateTeam(&model.Team{Id: 106})
match := model.Match{Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
arena.Database.CreateMatch(&match)
err := arena.LoadMatch(&match)
assert.Nil(t, err)
arena.AllianceStations["R1"].DsConn = &DriverStationConnection{TeamId: 101}
arena.AllianceStations["R2"].DsConn = &DriverStationConnection{TeamId: 102}
arena.AllianceStations["R3"].DsConn = &DriverStationConnection{TeamId: 103}
arena.AllianceStations["B1"].DsConn = &DriverStationConnection{TeamId: 104}
arena.AllianceStations["B2"].DsConn = &DriverStationConnection{TeamId: 105}
arena.AllianceStations["B3"].DsConn = &DriverStationConnection{TeamId: 106}
for _, station := range arena.AllianceStations {
station.DsConn.RobotLinked = true
}
err = arena.StartMatch()
assert.Nil(t, err)
arena.MatchState = PreMatch
// Check with a single team E-stopped, A-stopped, not linked, and bypassed.
arena.AllianceStations["R1"].EStop = true
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "while an emergency stop is active")
}
arena.AllianceStations["R1"].EStop = false
arena.AllianceStations["R1"].aStopReset = false
arena.AllianceStations["R1"].AStop = true
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "if an autonomous stop has not been reset since the previous match")
}
arena.AllianceStations["R1"].aStopReset = true
arena.AllianceStations["R1"].DsConn.RobotLinked = false
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "until all robots are connected or bypassed")
}
arena.AllianceStations["R1"].Bypass = true
err = arena.StartMatch()
assert.Nil(t, err)
arena.AllianceStations["R1"].Bypass = false
arena.MatchState = PreMatch
// Check with a team missing.
err = arena.assignTeam(0, "R1")
assert.Nil(t, err)
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "until all robots are connected or bypassed")
}
arena.AllianceStations["R1"].Bypass = true
err = arena.StartMatch()
assert.Nil(t, err)
arena.MatchState = PreMatch
// Check with no teams present.
arena.LoadMatch(new(model.Match))
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "until all robots are connected or bypassed")
}
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].Bypass = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B3"].Bypass = true
arena.AllianceStations["B3"].EStop = true
err = arena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "while an emergency stop is active")
}
arena.AllianceStations["B3"].EStop = false
err = arena.StartMatch()
assert.Nil(t, err)
}
func TestLoadNextMatch(t *testing.T) {
arena := setupTestArena(t)
arena.Database.CreateTeam(&model.Team{Id: 1114})
practiceMatch1 := model.Match{Type: model.Practice, TypeOrder: 1}
practiceMatch2 := model.Match{Type: model.Practice, TypeOrder: 2, Status: game.RedWonMatch}
practiceMatch3 := model.Match{Type: model.Practice, TypeOrder: 3}
arena.Database.CreateMatch(&practiceMatch1)
arena.Database.CreateMatch(&practiceMatch2)
arena.Database.CreateMatch(&practiceMatch3)
qualificationMatch1 := model.Match{Type: model.Qualification, TypeOrder: 1, Status: game.BlueWonMatch}
qualificationMatch2 := model.Match{Type: model.Qualification, TypeOrder: 2}
arena.Database.CreateMatch(&qualificationMatch1)
arena.Database.CreateMatch(&qualificationMatch2)
// Test match should be followed by another, empty test match.
assert.Equal(t, 0, arena.CurrentMatch.Id)
err := arena.SubstituteTeams(1114, 0, 0, 0, 0, 0)
assert.Nil(t, err)
arena.CurrentMatch.Status = game.TieMatch
err = arena.LoadNextMatch(false)
assert.Nil(t, err)
assert.Equal(t, 0, arena.CurrentMatch.Id)
assert.Equal(t, 0, arena.CurrentMatch.Red1)
assert.Equal(t, false, arena.CurrentMatch.IsComplete())
// Other matches should be loaded by type until they're all complete.
err = arena.LoadMatch(&practiceMatch2)
assert.Nil(t, err)
err = arena.LoadNextMatch(false)
assert.Nil(t, err)
assert.Equal(t, practiceMatch1.Id, arena.CurrentMatch.Id)
practiceMatch1.Status = game.RedWonMatch
arena.Database.UpdateMatch(&practiceMatch1)
err = arena.LoadNextMatch(false)
assert.Nil(t, err)
assert.Equal(t, practiceMatch3.Id, arena.CurrentMatch.Id)
practiceMatch3.Status = game.BlueWonMatch
arena.Database.UpdateMatch(&practiceMatch3)
err = arena.LoadNextMatch(false)
assert.Nil(t, err)
assert.Equal(t, 0, arena.CurrentMatch.Id)
assert.Equal(t, model.Test, arena.CurrentMatch.Type)
err = arena.LoadMatch(&qualificationMatch1)
assert.Nil(t, err)
err = arena.LoadNextMatch(false)
assert.Nil(t, err)
assert.Equal(t, qualificationMatch2.Id, arena.CurrentMatch.Id)
}
func TestSubstituteTeam(t *testing.T) {
arena := setupTestArena(t)
tournament.CreateTestAlliances(arena.Database, 2)
arena.PlayoffTournament, _ = playoff.NewPlayoffTournament(
arena.EventSettings.PlayoffType, arena.EventSettings.NumPlayoffAlliances,
)
arena.Database.CreateTeam(&model.Team{Id: 101})
arena.Database.CreateTeam(&model.Team{Id: 102})
arena.Database.CreateTeam(&model.Team{Id: 103})
arena.Database.CreateTeam(&model.Team{Id: 104})
arena.Database.CreateTeam(&model.Team{Id: 105})
arena.Database.CreateTeam(&model.Team{Id: 106})
arena.Database.CreateTeam(&model.Team{Id: 107})
// Substitute teams into test match.
err := arena.SubstituteTeams(0, 0, 0, 101, 0, 0)
assert.Nil(t, err)
assert.Equal(t, 101, arena.CurrentMatch.Blue1)
assert.Equal(t, 101, arena.AllianceStations["B1"].Team.Id)
err = arena.assignTeam(104, "R4")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Invalid alliance station")
}
// Substitute teams into practice match.
match := model.Match{Type: model.Practice, Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
arena.Database.CreateMatch(&match)
arena.LoadMatch(&match)
err = arena.SubstituteTeams(107, 102, 103, 104, 105, 106)
assert.Nil(t, err)
assert.Equal(t, 107, arena.CurrentMatch.Red1)
assert.Equal(t, 107, arena.AllianceStations["R1"].Team.Id)
matchResult := model.NewMatchResult()
matchResult.MatchId = arena.CurrentMatch.Id
// Check that substitution is disallowed in qualification matches.
match = model.Match{Type: model.Qualification, Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
arena.Database.CreateMatch(&match)
arena.LoadMatch(&match)
err = arena.SubstituteTeams(107, 102, 103, 104, 105, 106)
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Can't substitute teams for qualification matches.")
}
match = model.Match{Type: model.Playoff, Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
arena.Database.CreateMatch(&match)
arena.LoadMatch(&match)
assert.Nil(t, arena.SubstituteTeams(107, 102, 103, 104, 105, 106))
// Check that loading a nonexistent team fails.
err = arena.SubstituteTeams(101, 102, 103, 104, 105, 108)
if assert.NotNil(t, err) {
assert.Equal(t, err.Error(), "Team 108 is not present at the event.")
}
}
func TestLoadTeamsFromNexus(t *testing.T) {
arena := setupTestArena(t)
for i := 1; i <= 12; i++ {
arena.Database.CreateTeam(&model.Team{Id: 100 + i})
}
match := model.Match{
Type: model.Practice,
Red1: 101,
Red2: 102,
Red3: 103,
Blue1: 104,
Blue2: 105,
Blue3: 106,
TbaMatchKey: model.TbaMatchKey{CompLevel: "p", SetNumber: 0, MatchNumber: 1},
}
arena.Database.CreateMatch(&match)
assertTeams := func(red1, red2, red3, blue1, blue2, blue int) {
assert.Equal(t, red1, arena.CurrentMatch.Red1)
assert.Equal(t, red2, arena.CurrentMatch.Red2)
assert.Equal(t, red3, arena.CurrentMatch.Red3)
assert.Equal(t, blue1, arena.CurrentMatch.Blue1)
assert.Equal(t, blue2, arena.CurrentMatch.Blue2)
assert.Equal(t, blue, arena.CurrentMatch.Blue3)
assert.Equal(t, red1, arena.AllianceStations["R1"].Team.Id)
assert.Equal(t, red2, arena.AllianceStations["R2"].Team.Id)
assert.Equal(t, red3, arena.AllianceStations["R3"].Team.Id)
assert.Equal(t, blue1, arena.AllianceStations["B1"].Team.Id)
assert.Equal(t, blue2, arena.AllianceStations["B2"].Team.Id)
assert.Equal(t, blue, arena.AllianceStations["B3"].Team.Id)
}
// Sanity check that the match loads correctly without Nexus enabled.
assert.Nil(t, arena.LoadMatch(&match))
assertTeams(101, 102, 103, 104, 105, 106)
// Mock the Nexus server.
nexusServer := httptest.NewServer(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.String(), "/api/v1/event/my_event_code/match/p1/lineup") {
w.Write([]byte("{\"red\":[\"112\",\"111\",\"110\"],\"blue\":[\"109\",\"108\",\"107\"]}"))
} else {
http.Error(w, "Match not found", 404)
}
},
),
)
defer nexusServer.Close()
arena.NexusClient = partner.NewNexusClient("my_event_code")
arena.NexusClient.BaseUrl = nexusServer.URL
arena.EventSettings.NexusEnabled = true
// Check that the correct teams are loaded from Nexus.
assert.Nil(t, arena.LoadMatch(&match))
assertTeams(112, 111, 110, 109, 108, 107)
// Check with a match that Nexus doesn't know about.
match = model.Match{
Type: model.Practice,
Red1: 106,
Red2: 105,
Red3: 104,
Blue1: 103,
Blue2: 102,
Blue3: 101,
TbaMatchKey: model.TbaMatchKey{CompLevel: "p", SetNumber: 0, MatchNumber: 2},
}
arena.Database.CreateMatch(&match)
assert.Nil(t, arena.LoadMatch(&match))
assertTeams(106, 105, 104, 103, 102, 101)
}
func TestArenaTimeout(t *testing.T) {
arena := setupTestArena(t)
// Test regular ending of timeout.
timeoutDurationSec := 9
assert.Nil(t, arena.StartTimeout("Break 1", timeoutDurationSec))
assert.Equal(t, timeoutDurationSec, game.MatchTiming.TimeoutDurationSec)
assert.Equal(t, TimeoutActive, arena.MatchState)
assert.Equal(t, "Break 1", arena.breakDescription)
arena.MatchStartTime = time.Now().Add(-time.Duration(timeoutDurationSec) * time.Second)
arena.Update()
assert.Equal(t, PostTimeout, arena.MatchState)
arena.MatchStartTime = time.Now().Add(-time.Duration(timeoutDurationSec+postTimeoutSec) * time.Second)
arena.Update()
assert.Equal(t, PreMatch, arena.MatchState)
// Test early cancellation of timeout.
timeoutDurationSec = 28
assert.Nil(t, arena.StartTimeout("Break 2", timeoutDurationSec))
assert.Equal(t, "Break 2", arena.breakDescription)
assert.Equal(t, TimeoutActive, arena.MatchState)
assert.Equal(t, timeoutDurationSec, game.MatchTiming.TimeoutDurationSec)
assert.Nil(t, arena.AbortMatch())
arena.Update()
assert.Equal(t, PostTimeout, arena.MatchState)
arena.MatchStartTime = time.Now().Add(-time.Duration(timeoutDurationSec+postTimeoutSec) * time.Second)
arena.Update()
assert.Equal(t, PreMatch, arena.MatchState)
// Test that timeout can't be started during a match.
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].Bypass = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B3"].Bypass = true
assert.Nil(t, arena.StartMatch())
arena.Update()
assert.NotNil(t, arena.StartTimeout("Timeout", 1))
assert.NotEqual(t, TimeoutActive, arena.MatchState)
assert.Equal(t, timeoutDurationSec, game.MatchTiming.TimeoutDurationSec)
arena.MatchStartTime = time.Now().Add(
-time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+
game.MatchTiming.TeleopDurationSec,
) * time.Second,
)
for arena.MatchState != PostMatch {
arena.Update()
assert.NotNil(t, arena.StartTimeout("Timeout", 1))
}
// Test that a match can be loaded during a timeout.
assert.Nil(t, arena.ResetMatch())
assert.Nil(t, arena.LoadTestMatch())
assert.Nil(t, arena.StartTimeout("Break 2", 10))
assert.Equal(t, TimeoutActive, arena.MatchState)
match := model.Match{
Type: model.Playoff, ShortName: "F1", Red1: 1, Red2: 2, Red3: 3, Blue1: 4, Blue2: 5, Blue3: 6,
}
assert.Nil(t, arena.Database.CreateMatch(&match))
assert.Nil(t, arena.LoadMatch(&match))
assert.Equal(t, TimeoutActive, arena.MatchState)
assert.Equal(t, match, *arena.CurrentMatch)
}
func TestSaveTeamHasConnected(t *testing.T) {
arena := setupTestArena(t)
arena.Database.CreateTeam(&model.Team{Id: 101})
arena.Database.CreateTeam(&model.Team{Id: 102})
arena.Database.CreateTeam(&model.Team{Id: 103})
arena.Database.CreateTeam(&model.Team{Id: 104})
arena.Database.CreateTeam(&model.Team{Id: 105})
arena.Database.CreateTeam(&model.Team{Id: 106, City: "San Jose", HasConnected: true})
match := model.Match{Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
arena.Database.CreateMatch(&match)
arena.LoadMatch(&match)
arena.AllianceStations["R1"].DsConn = &DriverStationConnection{TeamId: 101}
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].DsConn = &DriverStationConnection{TeamId: 102, RobotLinked: true}
arena.AllianceStations["R3"].DsConn = &DriverStationConnection{TeamId: 103}
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].DsConn = &DriverStationConnection{TeamId: 104}
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].DsConn = &DriverStationConnection{TeamId: 105, RobotLinked: true}
arena.AllianceStations["B3"].DsConn = &DriverStationConnection{TeamId: 106, RobotLinked: true}
arena.AllianceStations["B3"].Team.City = "Sand Hosay" // Change some other field to verify that it isn't saved.
assert.Nil(t, arena.StartMatch())
// Check that the connection status was saved for the teams that just linked for the first time.
teams, _ := arena.Database.GetAllTeams()
if assert.Equal(t, 6, len(teams)) {
assert.False(t, teams[0].HasConnected)
assert.True(t, teams[1].HasConnected)
assert.False(t, teams[2].HasConnected)
assert.False(t, teams[3].HasConnected)
assert.True(t, teams[4].HasConnected)
assert.True(t, teams[5].HasConnected)
assert.Equal(t, "San Jose", teams[5].City)
}
}
func TestPlcEStopAStop(t *testing.T) {
arena := setupTestArena(t)
var plc FakePlc
plc.isEnabled = true
arena.Plc = &plc
arena.Database.CreateTeam(&model.Team{Id: 254})
err := arena.assignTeam(254, "R1")
assert.Nil(t, err)
dummyDs := &DriverStationConnection{TeamId: 254}
arena.AllianceStations["R1"].DsConn = dummyDs
arena.Database.CreateTeam(&model.Team{Id: 148})
err = arena.assignTeam(148, "R2")
assert.Nil(t, err)
dummyDs = &DriverStationConnection{TeamId: 148}
arena.AllianceStations["R2"].DsConn = dummyDs
arena.AllianceStations["R1"].DsConn.RobotLinked = true
arena.AllianceStations["R1"].aStopReset = true
arena.AllianceStations["R2"].DsConn.RobotLinked = true
arena.AllianceStations["R2"].aStopReset = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["R3"].aStopReset = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B1"].aStopReset = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B2"].aStopReset = true
arena.AllianceStations["B3"].Bypass = true
arena.AllianceStations["B3"].aStopReset = true
err = arena.StartMatch()
assert.Nil(t, err)
arena.Update()
arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
arena.Update()
assert.Equal(t, AutoPeriod, arena.MatchState)
assert.Equal(t, true, arena.AllianceStations["R1"].DsConn.Enabled)
// Press the R1 A-stop.
plc.redAStops[0] = true
plc.redEStops[0] = false
plc.redAStops[1] = false
plc.redEStops[1] = false
arena.Update()
assert.Equal(t, true, arena.AllianceStations["R1"].AStop)
assert.Equal(t, false, arena.AllianceStations["R1"].EStop)
assert.Equal(t, false, arena.AllianceStations["R2"].AStop)
assert.Equal(t, false, arena.AllianceStations["R2"].EStop)
arena.lastDsPacketTime = time.Unix(0, 0) // Force a DS packet.
arena.Update()
assert.Equal(t, false, arena.AllianceStations["R1"].DsConn.Enabled)
assert.Equal(t, false, arena.AllianceStations["R1"].DsConn.EStop)
assert.Equal(t, true, arena.AllianceStations["R1"].DsConn.AStop)
assert.Equal(t, true, arena.AllianceStations["R2"].DsConn.Enabled)
// Unpress the R1 A-stop and press the R2 E-stop.
plc.redAStops[0] = false
plc.redEStops[0] = false
plc.redAStops[1] = false
plc.redEStops[1] = true
arena.Update()
assert.Equal(t, true, arena.AllianceStations["R1"].AStop)
assert.Equal(t, false, arena.AllianceStations["R1"].EStop)
assert.Equal(t, false, arena.AllianceStations["R2"].AStop)
assert.Equal(t, true, arena.AllianceStations["R2"].EStop)
arena.lastDsPacketTime = time.Unix(0, 0) // Force a DS packet.
arena.Update()
assert.Equal(t, false, arena.AllianceStations["R1"].DsConn.Enabled)
assert.Equal(t, false, arena.AllianceStations["R1"].DsConn.EStop)
assert.Equal(t, true, arena.AllianceStations["R1"].DsConn.AStop)
assert.Equal(t, false, arena.AllianceStations["R2"].DsConn.Enabled)
assert.Equal(t, true, arena.AllianceStations["R2"].DsConn.EStop)
assert.Equal(t, false, arena.AllianceStations["R2"].DsConn.AStop)
// Unpress the R2 E-stop.
plc.redAStops[0] = false
plc.redEStops[0] = false
plc.redAStops[1] = false
plc.redEStops[1] = false
arena.Update()
assert.Equal(t, true, arena.AllianceStations["R1"].AStop)
assert.Equal(t, false, arena.AllianceStations["R1"].EStop)
assert.Equal(t, false, arena.AllianceStations["R2"].AStop)
assert.Equal(t, true, arena.AllianceStations["R2"].EStop)
arena.lastDsPacketTime = time.Unix(0, 0) // Force a DS packet.
arena.Update()
assert.Equal(t, false, arena.AllianceStations["R1"].DsConn.Enabled)
assert.Equal(t, false, arena.AllianceStations["R2"].DsConn.Enabled)
// Transition into the teleop period without any stops.
arena.MatchStartTime = time.Now().Add(
-time.Duration(game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec) * time.Second,
)
arena.Update()
assert.Equal(t, PausePeriod, arena.MatchState)
arena.MatchStartTime = time.Now().Add(
-time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec,
) * time.Second,
)
arena.Update()
assert.Equal(t, false, arena.AllianceStations["R1"].AStop)
assert.Equal(t, false, arena.AllianceStations["R1"].EStop)
assert.Equal(t, false, arena.AllianceStations["R2"].AStop)
assert.Equal(t, true, arena.AllianceStations["R2"].EStop)
arena.lastDsPacketTime = time.Unix(0, 0) // Force a DS packet.
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, true, arena.AllianceStations["R1"].DsConn.Enabled)
assert.Equal(t, false, arena.AllianceStations["R2"].DsConn.Enabled)
// Press the R1 E-stop and the R2 A-stop.
plc.redAStops[0] = false
plc.redEStops[0] = true
plc.redAStops[1] = true
plc.redEStops[1] = false
arena.Update()
assert.Equal(t, false, arena.AllianceStations["R1"].AStop)
assert.Equal(t, true, arena.AllianceStations["R1"].EStop)
assert.Equal(t, true, arena.AllianceStations["R2"].AStop)
assert.Equal(t, true, arena.AllianceStations["R2"].EStop)
arena.lastDsPacketTime = time.Unix(0, 0) // Force a DS packet.
arena.Update()
assert.Equal(t, false, arena.AllianceStations["R1"].DsConn.Enabled)
assert.Equal(t, false, arena.AllianceStations["R2"].DsConn.Enabled)
// Ensure the other stations A-stops are working as well.
plc.redAStops[2] = true
plc.redEStops[2] = false
plc.blueAStops[0] = true
plc.blueEStops[0] = false
plc.blueAStops[1] = true
plc.blueEStops[1] = false
plc.blueAStops[2] = true
plc.blueEStops[2] = false
arena.Update()
assert.Equal(t, true, arena.AllianceStations["R3"].AStop)
assert.Equal(t, false, arena.AllianceStations["R3"].EStop)
assert.Equal(t, true, arena.AllianceStations["B1"].AStop)
assert.Equal(t, false, arena.AllianceStations["B1"].EStop)
assert.Equal(t, true, arena.AllianceStations["B2"].AStop)
assert.Equal(t, false, arena.AllianceStations["B2"].EStop)
assert.Equal(t, true, arena.AllianceStations["B3"].AStop)
assert.Equal(t, false, arena.AllianceStations["B3"].EStop)
// Ensure the other stations E-stops are working as well.
plc.redAStops[2] = false
plc.redEStops[2] = true
plc.blueAStops[0] = false
plc.blueEStops[0] = true
plc.blueAStops[1] = false
plc.blueEStops[1] = true
plc.blueAStops[2] = false
plc.blueEStops[2] = true
arena.Update()
assert.Equal(t, false, arena.AllianceStations["R3"].AStop)
assert.Equal(t, true, arena.AllianceStations["R3"].EStop)
assert.Equal(t, false, arena.AllianceStations["B1"].AStop)
assert.Equal(t, true, arena.AllianceStations["B1"].EStop)
assert.Equal(t, false, arena.AllianceStations["B2"].AStop)
assert.Equal(t, true, arena.AllianceStations["B2"].EStop)
assert.Equal(t, false, arena.AllianceStations["B3"].AStop)
assert.Equal(t, true, arena.AllianceStations["B3"].EStop)
// Ensure unpressed E-stops are cleared at the end of the match.
arena.MatchStartTime = time.Now().Add(
-time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+
game.MatchTiming.TeleopDurationSec,
) * time.Second,
)
arena.Update()
plc.blueEStops[2] = false
arena.Update()
assert.Equal(t, true, arena.AllianceStations["R1"].EStop)
assert.Equal(t, false, arena.AllianceStations["R2"].EStop)
assert.Equal(t, true, arena.AllianceStations["R3"].EStop)
assert.Equal(t, true, arena.AllianceStations["B1"].EStop)
assert.Equal(t, true, arena.AllianceStations["B2"].EStop)
assert.Equal(t, false, arena.AllianceStations["B3"].EStop)
}
func TestPlcEStopAStopWithPlcDisabled(t *testing.T) {
arena := setupTestArena(t)
var plc FakePlc
plc.isEnabled = false
arena.Plc = &plc
arena.Database.CreateTeam(&model.Team{Id: 254})
err := arena.assignTeam(254, "R1")
assert.Nil(t, err)
arena.AllianceStations["R1"].DsConn = &DriverStationConnection{TeamId: 254}
arena.AllianceStations["R2"].DsConn = &DriverStationConnection{TeamId: 1323}
arena.AllianceStations["R1"].DsConn.RobotLinked = true
arena.AllianceStations["R2"].DsConn.RobotLinked = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B3"].Bypass = true
assert.Nil(t, arena.StartMatch())
arena.Update()
arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
arena.Update()
assert.Equal(t, AutoPeriod, arena.MatchState)
assert.Equal(t, true, arena.AllianceStations["R1"].DsConn.Enabled)
plc.redEStops[0] = true
plc.redAStops[1] = true
arena.Update()
assert.Equal(t, false, arena.AllianceStations["R1"].AStop)
assert.Equal(t, false, arena.AllianceStations["R1"].EStop)
assert.Equal(t, true, arena.AllianceStations["R1"].DsConn.Enabled)
assert.Equal(t, false, arena.AllianceStations["R2"].AStop)
assert.Equal(t, false, arena.AllianceStations["R2"].EStop)
assert.Equal(t, true, arena.AllianceStations["R2"].DsConn.Enabled)
}
func TestPlcFieldEStop(t *testing.T) {
arena := setupTestArena(t)
var plc FakePlc
plc.isEnabled = true
arena.Plc = &plc
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].Bypass = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B3"].Bypass = true
assert.Nil(t, arena.StartMatch())
arena.Update()
arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
arena.Update()
assert.Equal(t, AutoPeriod, arena.MatchState)
plc.fieldEStop = true
arena.Update()
assert.True(t, arena.matchAborted)
assert.Equal(t, PostMatch, arena.MatchState)
}
func TestPlcFieldEStopWithPlcDisabled(t *testing.T) {
arena := setupTestArena(t)
var plc FakePlc
plc.isEnabled = false
arena.Plc = &plc
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].Bypass = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B3"].Bypass = true
assert.Nil(t, arena.StartMatch())
arena.Update()
arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
arena.Update()
assert.Equal(t, AutoPeriod, arena.MatchState)
plc.fieldEStop = true
arena.Update()
assert.False(t, arena.matchAborted)
assert.Equal(t, AutoPeriod, arena.MatchState)
}
func TestPlcMatchCycleEvergreen(t *testing.T) {
arena := setupTestArena(t)
var plc FakePlc
plc.isEnabled = true
arena.Plc = &plc
arena.Update()
assert.Equal(t, [4]bool{true, true, false, false}, plc.stackLights)
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.Update()
assert.Equal(t, [4]bool{true, true, false, false}, plc.stackLights)
arena.AllianceStations["R3"].Bypass = true
arena.Update()
assert.Equal(t, [4]bool{false, true, false, false}, plc.stackLights)
assert.Equal(t, false, plc.stackLightBuzzer)
// All teams are ready.
arena.AllianceStations["B3"].Bypass = true
plc.cycleState = true
arena.Update()
assert.Equal(t, [4]bool{false, false, false, true}, plc.stackLights)
assert.Equal(t, true, plc.stackLightBuzzer)
// Green light when blink cycle is off.
plc.cycleState = false
arena.Update()
assert.Equal(t, [4]bool{false, false, false, false}, plc.stackLights)
// Start the match.
assert.Nil(t, arena.StartMatch())
arena.Update()
arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
arena.Update()
assert.Equal(t, AutoPeriod, arena.MatchState)
assert.Equal(t, [4]bool{false, false, false, true}, plc.stackLights)
assert.Equal(t, false, plc.stackLightBuzzer)
// End the match.
arena.MatchStartTime = time.Now().Add(
-time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+
game.MatchTiming.TeleopDurationSec,
) * time.Second,
)
arena.Update()
arena.Update()
arena.Update()
assert.Equal(t, PostMatch, arena.MatchState)
assert.Equal(t, [4]bool{false, false, true, false}, plc.stackLights)
assert.Equal(t, false, plc.fieldResetLight)
// Ready the score.
arena.RedRealtimeScore.FoulsCommitted = true
arena.BlueRealtimeScore.FoulsCommitted = true
redWs := &websocket.Websocket{}
arena.ScoringPanelRegistry.RegisterPanel("red_near", redWs)
arena.ScoringPanelRegistry.SetScoreCommitted("red_near", redWs)
arena.Update()
assert.Equal(t, [4]bool{false, false, true, false}, plc.stackLights)
blueWs := &websocket.Websocket{}
arena.ScoringPanelRegistry.RegisterPanel("blue_far", blueWs)
arena.ScoringPanelRegistry.SetScoreCommitted("blue_far", blueWs)
arena.Update()
assert.Equal(t, [4]bool{false, false, true, false}, plc.stackLights)
arena.ScoringPanelRegistry.RegisterPanel("blue_near", redWs)
arena.ScoringPanelRegistry.SetScoreCommitted("blue_near", redWs)
arena.Update()
assert.Equal(t, [4]bool{false, false, true, false}, plc.stackLights)
arena.ScoringPanelRegistry.RegisterPanel("red_far", redWs)
arena.ScoringPanelRegistry.SetScoreCommitted("red_far", redWs)
arena.Update()
assert.Equal(t, [4]bool{false, false, false, false}, plc.stackLights)
arena.FieldReset = true
arena.Update()
assert.Equal(t, true, plc.fieldResetLight)
}
func TestPlcMatchCycleGameSpecificWithCoopEnabled(t *testing.T) {
arena := setupTestArena(t)
var plc FakePlc
plc.isEnabled = true
arena.Plc = &plc
// Check that no inputs or outputs are active before the match starts.
assert.Equal(t, PreMatch, arena.MatchState)
plc.redProcessorCount = 5
plc.blueProcessorCount = 8
arena.Update()
redScore := &arena.RedRealtimeScore.CurrentScore
blueScore := &arena.BlueRealtimeScore.CurrentScore
assert.Equal(t, 0, redScore.ProcessorAlgae)
assert.Equal(t, 0, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{false, false, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, false, false}, plc.blueTrussLights)
plc.redProcessorCount = 0
plc.blueProcessorCount = 0
// Start the match.
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].Bypass = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B3"].Bypass = true
arena.Update()
assert.Nil(t, arena.StartMatch())
arena.Update()
arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
arena.Update()
assert.Equal(t, AutoPeriod, arena.MatchState)
// Check the autonomous period.
plc.redProcessorCount = 1
arena.Update()
assert.Equal(t, 1, redScore.ProcessorAlgae)
assert.Equal(t, 0, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, false, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, false, false}, plc.blueTrussLights)
// Check the pause period.
arena.MatchStartTime = time.Now().Add(
-time.Duration(game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec) * time.Second,
)
arena.Update()
assert.Equal(t, PausePeriod, arena.MatchState)
plc.redProcessorCount = 2
arena.Update()
assert.Equal(t, 2, redScore.ProcessorAlgae)
assert.Equal(t, 0, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, false, false}, plc.blueTrussLights)
// Check the teleop period.
durationToTeleopStart := time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec,
) * time.Second
arena.MatchStartTime = time.Now().Add(-durationToTeleopStart - 5000*time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
plc.blueProcessorCount = 1
arena.Update()
assert.Equal(t, 2, redScore.ProcessorAlgae)
assert.Equal(t, 1, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, false, false}, plc.blueTrussLights)
plc.redProcessorCount = 3
arena.Update()
assert.Equal(t, 3, redScore.ProcessorAlgae)
assert.Equal(t, 1, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, false, false}, plc.blueTrussLights)
plc.redProcessorCount = 17
arena.Update()
assert.Equal(t, 17, redScore.ProcessorAlgae)
assert.Equal(t, 1, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, false, false}, plc.blueTrussLights)
assert.Equal(t, true, arena.RedScoreSummary().CoopertitionCriteriaMet)
assert.Equal(t, false, arena.RedScoreSummary().CoopertitionBonus)
assert.Equal(t, false, arena.BlueScoreSummary().CoopertitionCriteriaMet)
assert.Equal(t, false, arena.BlueScoreSummary().CoopertitionBonus)
plc.blueProcessorCount = 2
arena.Update()
assert.Equal(t, 17, redScore.ProcessorAlgae)
assert.Equal(t, 2, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
assert.Equal(t, true, arena.RedScoreSummary().CoopertitionCriteriaMet)
assert.Equal(t, true, arena.RedScoreSummary().CoopertitionBonus)
assert.Equal(t, true, arena.BlueScoreSummary().CoopertitionCriteriaMet)
assert.Equal(t, true, arena.BlueScoreSummary().CoopertitionBonus)
// Check the truss lights during the "sonar ping" warning sound.
durationToWarning := time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+
game.MatchTiming.TeleopDurationSec-game.MatchTiming.WarningRemainingDurationSec,
) * time.Second
arena.MatchStartTime = time.Now().Add(-durationToWarning - 100*time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, [3]bool{true, false, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, false, false}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToWarning - 300*time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, [3]bool{false, true, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, true, false}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToWarning - 500*time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, [3]bool{false, false, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, false, true}, plc.blueTrussLights)
// Undo the co-op and check that the truss lights are back to normal after the warning.
arena.MatchStartTime = time.Now().Add(-durationToWarning - 5000*time.Millisecond)
plc.redProcessorCount = 1
arena.Update()
assert.Equal(t, 1, redScore.ProcessorAlgae)
assert.Equal(t, 2, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, false, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, false}, plc.blueTrussLights)
// Check after the end of the match.
durationToTeleopEnd := time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+
game.MatchTiming.TeleopDurationSec,
) * time.Second
arena.MatchStartTime = time.Now().Add(-durationToTeleopEnd + 1*time.Millisecond)
arena.Update()
assert.Equal(t, [3]bool{true, false, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, false}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToTeleopEnd - 1*time.Millisecond)
arena.Update()
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToTeleopEnd - 2999*time.Millisecond)
arena.Update()
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToTeleopEnd - 3001*time.Millisecond)
arena.Update()
assert.Equal(t, [3]bool{false, false, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, false, false}, plc.blueTrussLights)
}
func TestPlcMatchCycleGameSpecificWithCoopDisabled(t *testing.T) {
defer func() {
game.CoralBonusCoopEnabled = true
}()
testCases := []struct {
coopEnabled bool
matchType model.MatchType
}{
// 0. Playoff match with co-op enabled.
{true, model.Playoff},
// 1. Playoff match with co-op disabled.
{false, model.Playoff},
// 2. Qualification match with co-op disabled.
{false, model.Qualification},
}
for i, tc := range testCases {
t.Run(
strconv.Itoa(i),
func(t *testing.T) {
arena := setupTestArena(t)
var plc FakePlc
plc.isEnabled = true
arena.Plc = &plc
game.CoralBonusCoopEnabled = tc.coopEnabled
arena.CurrentMatch.Type = tc.matchType
// Check that no inputs or outputs are active before the match starts.
assert.Equal(t, PreMatch, arena.MatchState)
plc.redProcessorCount = 5
plc.blueProcessorCount = 8
arena.Update()
redScore := &arena.RedRealtimeScore.CurrentScore
blueScore := &arena.BlueRealtimeScore.CurrentScore
assert.Equal(t, 0, redScore.ProcessorAlgae)
assert.Equal(t, 0, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{false, false, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, false, false}, plc.blueTrussLights)
plc.redProcessorCount = 0
plc.blueProcessorCount = 0
// Start the match.
arena.AllianceStations["R1"].Bypass = true
arena.AllianceStations["R2"].Bypass = true
arena.AllianceStations["R3"].Bypass = true
arena.AllianceStations["B1"].Bypass = true
arena.AllianceStations["B2"].Bypass = true
arena.AllianceStations["B3"].Bypass = true
arena.Update()
assert.Nil(t, arena.StartMatch())
arena.Update()
arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
arena.Update()
assert.Equal(t, AutoPeriod, arena.MatchState)
// Check the autonomous period.
plc.redProcessorCount = 1
arena.Update()
assert.Equal(t, 1, redScore.ProcessorAlgae)
assert.Equal(t, 0, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
// Check the pause period.
arena.MatchStartTime = time.Now().Add(
-time.Duration(game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec) * time.Second,
)
arena.Update()
assert.Equal(t, PausePeriod, arena.MatchState)
plc.redProcessorCount = 2
arena.Update()
assert.Equal(t, 2, redScore.ProcessorAlgae)
assert.Equal(t, 0, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
// Check the teleop period.
durationToTeleopStart := time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec,
) * time.Second
arena.MatchStartTime = time.Now().Add(-durationToTeleopStart - 5000*time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
plc.blueProcessorCount = 1
arena.Update()
assert.Equal(t, 2, redScore.ProcessorAlgae)
assert.Equal(t, 1, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
plc.redProcessorCount = 3
arena.Update()
assert.Equal(t, 3, redScore.ProcessorAlgae)
assert.Equal(t, 1, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
plc.redProcessorCount = 17
arena.Update()
assert.Equal(t, 17, redScore.ProcessorAlgae)
assert.Equal(t, 1, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
plc.blueProcessorCount = 2
arena.Update()
assert.Equal(t, 17, redScore.ProcessorAlgae)
assert.Equal(t, 2, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
// Check the truss lights during the "sonar ping" warning sound.
durationToWarning := time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+
game.MatchTiming.TeleopDurationSec-game.MatchTiming.WarningRemainingDurationSec,
) * time.Second
arena.MatchStartTime = time.Now().Add(-durationToWarning - 100*time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, [3]bool{true, false, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, false, false}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToWarning - 300*time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, [3]bool{false, true, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, true, false}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToWarning - 500*time.Millisecond)
arena.Update()
assert.Equal(t, TeleopPeriod, arena.MatchState)
assert.Equal(t, [3]bool{false, false, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, false, true}, plc.blueTrussLights)
// Undo the co-op and check that the truss lights are back to normal after the warning.
arena.MatchStartTime = time.Now().Add(-durationToWarning - 5000*time.Millisecond)
plc.redProcessorCount = 1
arena.Update()
assert.Equal(t, 1, redScore.ProcessorAlgae)
assert.Equal(t, 2, blueScore.ProcessorAlgae)
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
// Check after the end of the match.
durationToTeleopEnd := time.Duration(
game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+
game.MatchTiming.TeleopDurationSec,
) * time.Second
arena.MatchStartTime = time.Now().Add(-durationToTeleopEnd + 1*time.Millisecond)
arena.Update()
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToTeleopEnd - 1*time.Millisecond)
arena.Update()
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToTeleopEnd - 2999*time.Millisecond)
arena.Update()
assert.Equal(t, [3]bool{true, true, true}, plc.redTrussLights)
assert.Equal(t, [3]bool{true, true, true}, plc.blueTrussLights)
arena.MatchStartTime = time.Now().Add(-durationToTeleopEnd - 3001*time.Millisecond)
arena.Update()
assert.Equal(t, [3]bool{false, false, false}, plc.redTrussLights)
assert.Equal(t, [3]bool{false, false, false}, plc.blueTrussLights)
},
)
}
}
================================================
FILE: field/display.go
================================================
// Copyright 2018 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model representing and methods for controlling a remote web display.
package field
import (
"fmt"
"github.com/Team254/cheesy-arena/websocket"
"net/url"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
)
const (
minDisplayId = 100
displayPurgeTtlMin = 30
)
type DisplayType int
const (
InvalidDisplay DisplayType = iota
PlaceholderDisplay
AllianceStationDisplay
AnnouncerDisplay
AudienceDisplay
BracketDisplay
FieldMonitorDisplay
LogoDisplay
QueueingDisplay
RankingsDisplay
TwitchStreamDisplay
WallDisplay
WebpageDisplay
)
var DisplayTypeNames = map[DisplayType]string{
PlaceholderDisplay: "Placeholder",
AllianceStationDisplay: "Alliance Station",
AnnouncerDisplay: "Announcer",
AudienceDisplay: "Audience",
BracketDisplay: "Bracket",
FieldMonitorDisplay: "Field Monitor",
LogoDisplay: "Logo",
QueueingDisplay: "Queueing",
RankingsDisplay: "Rankings",
TwitchStreamDisplay: "Twitch Stream",
WallDisplay: "Wall",
WebpageDisplay: "Web Page",
}
var displayTypePaths = map[DisplayType]string{
PlaceholderDisplay: "/display",
AllianceStationDisplay: "/displays/alliance_station",
AnnouncerDisplay: "/displays/announcer",
AudienceDisplay: "/displays/audience",
BracketDisplay: "/displays/bracket",
FieldMonitorDisplay: "/displays/field_monitor",
LogoDisplay: "/displays/logo",
QueueingDisplay: "/displays/queueing",
RankingsDisplay: "/displays/rankings",
TwitchStreamDisplay: "/displays/twitch",
WallDisplay: "/displays/wall",
WebpageDisplay: "/displays/webpage",
}
var displayRegistryMutex sync.Mutex
type Display struct {
DisplayConfiguration DisplayConfiguration
IpAddress string
ConnectionCount int
Notifier *websocket.Notifier
lastConnectedTime time.Time
}
type DisplayConfiguration struct {
Id string
Nickname string
Type DisplayType
Configuration map[string]string
}
// Parses the given display URL path and query string to extract the configuration.
func DisplayFromUrl(path string, query map[string][]string) (*DisplayConfiguration, error) {
if _, ok := query["displayId"]; !ok {
return nil, fmt.Errorf("Display ID not present in request.")
}
var displayConfig DisplayConfiguration
displayConfig.Id = query["displayId"][0]
if nickname, ok := query["nickname"]; ok {
displayConfig.Nickname, _ = url.QueryUnescape(nickname[0])
}
// Determine type from the websocket connection URL. This way of doing it isn't super efficient, but it's not really
// a concern since it should happen relatively infrequently.
for displayType, displayPath := range displayTypePaths {
if path == displayPath+"/websocket" {
displayConfig.Type = displayType
break
}
}
if displayConfig.Type == InvalidDisplay {
return nil, fmt.Errorf("Could not determine display type from path %s.", path)
}
// Put any remaining query parameters into the per-type configuration map.
displayConfig.Configuration = make(map[string]string)
for key, value := range query {
if key != "displayId" && key != "nickname" {
displayConfig.Configuration[key], _ = url.QueryUnescape(value[0])
}
}
return &displayConfig, nil
}
// Returns the URL string for the given display that includes all of its configuration parameters.
func (display *Display) ToUrl() string {
var builder strings.Builder
builder.WriteString(displayTypePaths[display.DisplayConfiguration.Type])
builder.WriteString("?displayId=")
builder.WriteString(url.QueryEscape(display.DisplayConfiguration.Id))
if display.DisplayConfiguration.Nickname != "" {
builder.WriteString("&nickname=")
builder.WriteString(url.QueryEscape(display.DisplayConfiguration.Nickname))
}
// Sort the keys so that the URL generated is deterministic.
var keys []string
for key := range display.DisplayConfiguration.Configuration {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
builder.WriteString("&")
builder.WriteString(url.QueryEscape(key))
builder.WriteString("=")
builder.WriteString(url.QueryEscape(display.DisplayConfiguration.Configuration[key]))
}
return builder.String()
}
func (display *Display) generateDisplayConfigurationMessage() any {
return display.ToUrl()
}
// Returns an unused ID that can be used for a new display.
func (arena *Arena) NextDisplayId() string {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
// Loop until we get an ID that isn't already used. This is inefficient if there is a large number of displays, but
// that should never be the case.
candidateId := minDisplayId
for {
if _, ok := arena.Displays[strconv.Itoa(candidateId)]; !ok {
return strconv.Itoa(candidateId)
}
candidateId++
}
}
// Creates or gets the given display in the arena registry and triggers a notification.
func (arena *Arena) RegisterDisplay(displayConfig *DisplayConfiguration, ipAddress string) *Display {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
display, ok := arena.Displays[displayConfig.Id]
if ok && displayConfig.Type == PlaceholderDisplay {
// Don't rewrite the registered configuration if the new one is a placeholder -- if it is reconnecting after a
// restart, it should adopt the existing configuration.
arena.Displays[displayConfig.Id].ConnectionCount++
arena.Displays[displayConfig.Id].IpAddress = ipAddress
} else {
if !ok {
display = new(Display)
display.Notifier = websocket.NewNotifier(
"displayConfiguration", display.generateDisplayConfigurationMessage,
)
arena.Displays[displayConfig.Id] = display
}
display.DisplayConfiguration = *displayConfig
display.IpAddress = ipAddress
display.ConnectionCount += 1
display.lastConnectedTime = time.Now()
display.Notifier.Notify()
}
arena.DisplayConfigurationNotifier.Notify()
return display
}
// Updates the given display in the arena registry. Triggers a notification if the display configuration changed.
func (arena *Arena) UpdateDisplay(displayConfig DisplayConfiguration) error {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
display, ok := arena.Displays[displayConfig.Id]
if !ok {
return fmt.Errorf("Display %s doesn't exist.", displayConfig.Id)
}
if !reflect.DeepEqual(displayConfig, display.DisplayConfiguration) {
display.DisplayConfiguration = displayConfig
display.Notifier.Notify()
arena.DisplayConfigurationNotifier.Notify()
}
return nil
}
// Marks the given display as having disconnected in the arena registry and triggers a notification.
func (arena *Arena) MarkDisplayDisconnected(displayId string) {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
if existingDisplay, ok := arena.Displays[displayId]; ok {
if existingDisplay.ConnectionCount == 1 && existingDisplay.DisplayConfiguration.Type == PlaceholderDisplay &&
existingDisplay.DisplayConfiguration.Nickname == "" &&
len(existingDisplay.DisplayConfiguration.Configuration) == 0 {
// If the display is an unconfigured placeholder, just remove it entirely to prevent clutter.
delete(arena.Displays, existingDisplay.DisplayConfiguration.Id)
} else {
existingDisplay.ConnectionCount -= 1
}
existingDisplay.lastConnectedTime = time.Now()
arena.DisplayConfigurationNotifier.Notify()
}
}
// Removes any displays from the list that haven't had any active connections for a while and don't have a nickname.
func (arena *Arena) purgeDisconnectedDisplays() {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
deleted := false
for id, display := range arena.Displays {
if display.ConnectionCount == 0 && display.DisplayConfiguration.Nickname == "" &&
time.Now().Sub(display.lastConnectedTime).Minutes() >= displayPurgeTtlMin {
delete(arena.Displays, id)
deleted = true
}
}
if deleted {
arena.DisplayConfigurationNotifier.Notify()
}
}
================================================
FILE: field/display_test.go
================================================
// Copyright 2018 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package field
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestDisplayFromUrl(t *testing.T) {
query := map[string][]string{}
display, err := DisplayFromUrl("/display", query)
assert.Nil(t, display)
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "ID not present")
}
// Test the various types.
query["displayId"] = []string{"123"}
display, err = DisplayFromUrl("/blorpy", query)
assert.Nil(t, display)
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Could not determine display type")
}
display, _ = DisplayFromUrl("/display/websocket", query)
assert.Equal(t, PlaceholderDisplay, display.Type)
display, _ = DisplayFromUrl("/displays/alliance_station/websocket", query)
assert.Equal(t, AllianceStationDisplay, display.Type)
display, _ = DisplayFromUrl("/displays/announcer/websocket", query)
assert.Equal(t, AnnouncerDisplay, display.Type)
display, _ = DisplayFromUrl("/displays/audience/websocket", query)
assert.Equal(t, AudienceDisplay, display.Type)
display, _ = DisplayFromUrl("/displays/field_monitor/websocket", query)
assert.Equal(t, FieldMonitorDisplay, display.Type)
display, _ = DisplayFromUrl("/displays/rankings/websocket", query)
assert.Equal(t, RankingsDisplay, display.Type)
// Test the nickname and arbitrary parameters.
query["nickname"] = []string{"Test Nickname"}
query["key1"] = []string{"value1"}
query["key2"] = []string{"value2"}
query["color"] = []string{"%230f0"}
display, _ = DisplayFromUrl("/display/websocket", query)
assert.Equal(t, "Test Nickname", display.Nickname)
if assert.Equal(t, 3, len(display.Configuration)) {
assert.Equal(t, "value1", display.Configuration["key1"])
assert.Equal(t, "value2", display.Configuration["key2"])
assert.Equal(t, "#0f0", display.Configuration["color"])
}
}
func TestDisplayToUrl(t *testing.T) {
display := &Display{
DisplayConfiguration: DisplayConfiguration{
Id: "254",
Nickname: "Test Nickname",
Type: RankingsDisplay,
Configuration: map[string]string{"f": "1", "z": "#fff", "a": "3", "c": "4"},
},
}
assert.Equal(t, "/displays/rankings?displayId=254&nickname=Test+Nickname&a=3&c=4&f=1&z=%23fff", display.ToUrl())
}
func TestNextDisplayId(t *testing.T) {
arena := setupTestArena(t)
assert.Equal(t, "100", arena.NextDisplayId())
displayConfig := &DisplayConfiguration{Id: "100"}
arena.RegisterDisplay(displayConfig, "")
assert.Equal(t, "101", arena.NextDisplayId())
}
func TestDisplayRegisterUnregister(t *testing.T) {
arena := setupTestArena(t)
displayConfig := &DisplayConfiguration{
Id: "254",
Nickname: "Placeholder",
Type: PlaceholderDisplay,
Configuration: map[string]string{},
}
arena.RegisterDisplay(displayConfig, "1.2.3.4")
if assert.Contains(t, arena.Displays, "254") {
assert.Equal(t, "Placeholder", arena.Displays["254"].DisplayConfiguration.Nickname)
assert.Equal(t, PlaceholderDisplay, arena.Displays["254"].DisplayConfiguration.Type)
assert.Equal(t, 1, arena.Displays["254"].ConnectionCount)
assert.Equal(t, "1.2.3.4", arena.Displays["254"].IpAddress)
}
notifier := arena.Displays["254"].Notifier
// Register a second instance of the same display.
displayConfig2 := &DisplayConfiguration{
Id: "254",
Nickname: "Rankings",
Type: RankingsDisplay,
Configuration: map[string]string{},
}
arena.RegisterDisplay(displayConfig2, "2.3.4.5")
if assert.Contains(t, arena.Displays, "254") {
assert.Equal(t, "Rankings", arena.Displays["254"].DisplayConfiguration.Nickname)
assert.Equal(t, RankingsDisplay, arena.Displays["254"].DisplayConfiguration.Type)
assert.Equal(t, 2, arena.Displays["254"].ConnectionCount)
assert.Equal(t, "2.3.4.5", arena.Displays["254"].IpAddress)
assert.Same(t, notifier, arena.Displays["254"].Notifier)
}
// Register a second display.
displayConfig3 := &DisplayConfiguration{Id: "148", Type: FieldMonitorDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(displayConfig3, "3.4.5.6")
if assert.Contains(t, arena.Displays, "148") {
assert.Equal(t, 1, arena.Displays["148"].ConnectionCount)
}
// Update the first display.
displayConfig4 := DisplayConfiguration{
Id: "254",
Nickname: "Alliance",
Type: AllianceStationDisplay,
Configuration: map[string]string{"station": "B2"},
}
arena.UpdateDisplay(displayConfig4)
if assert.Contains(t, arena.Displays, "254") {
assert.Equal(t, "Alliance", arena.Displays["254"].DisplayConfiguration.Nickname)
assert.Equal(t, AllianceStationDisplay, arena.Displays["254"].DisplayConfiguration.Type)
assert.Equal(t, 2, arena.Displays["254"].ConnectionCount)
}
// Disconnect both displays.
arena.MarkDisplayDisconnected(displayConfig.Id)
arena.MarkDisplayDisconnected(displayConfig3.Id)
if assert.Contains(t, arena.Displays, "148") {
assert.Equal(t, 0, arena.Displays["148"].ConnectionCount)
}
if assert.Contains(t, arena.Displays, "254") {
assert.Equal(t, 1, arena.Displays["254"].ConnectionCount)
}
}
func TestDisplayUpdateError(t *testing.T) {
arena := setupTestArena(t)
displayConfig := DisplayConfiguration{Id: "254", Configuration: map[string]string{}}
err := arena.UpdateDisplay(displayConfig)
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "doesn't exist")
}
}
func TestDisplayPurge(t *testing.T) {
arena := setupTestArena(t)
// Unnamed placeholder gets immediately purged upon disconnection.
displayConfig := &DisplayConfiguration{Id: "254", Type: PlaceholderDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "254")
arena.MarkDisplayDisconnected(displayConfig.Id)
assert.NotContains(t, arena.Displays, "254")
// Named placeholder does not get immediately purged upon disconnection.
displayConfig.Nickname = "Bob"
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "254")
arena.MarkDisplayDisconnected(displayConfig.Id)
assert.Contains(t, arena.Displays, "254")
// Unnamed configured displayConfig does not get immediately purged upon disconnection.
displayConfig = &DisplayConfiguration{Id: "1114", Type: FieldMonitorDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "1114")
arena.MarkDisplayDisconnected(displayConfig.Id)
assert.Contains(t, arena.Displays, "1114")
arena.purgeDisconnectedDisplays()
assert.Contains(t, arena.Displays, "1114")
// Unnamed configured displayConfig gets purged by periodic task.
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "1114")
arena.MarkDisplayDisconnected(displayConfig.Id)
arena.Displays["1114"].lastConnectedTime = time.Now().Add(-displayPurgeTtlMin * time.Minute)
arena.purgeDisconnectedDisplays()
assert.NotContains(t, arena.Displays, "1114")
// Named configured displayConfig does not get purged by periodic task.
displayConfig.Nickname = "Brunhilda"
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "1114")
arena.MarkDisplayDisconnected(displayConfig.Id)
arena.Displays["1114"].lastConnectedTime = time.Now().Add(-displayPurgeTtlMin * time.Minute)
arena.purgeDisconnectedDisplays()
assert.Contains(t, arena.Displays, "1114")
}
================================================
FILE: field/driver_station_connection.go
================================================
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model and methods for interacting with a team's Driver Station.
package field
import (
"errors"
"fmt"
"io"
"log"
"net"
"time"
"github.com/Team254/cheesy-arena/game"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/network"
)
// FMS uses 1121 for sending UDP packets, and FMS Lite uses 1120. Using 1121
// seems to work just fine and doesn't prompt to let FMS take control.
const (
driverStationTcpListenPort = 1750
driverStationUdpSendPort = 1121
driverStationUdpSendPortLite = 1120
driverStationUdpReceivePort = 1160
driverStationTcpLinkTimeoutSec = 5
driverStationUdpLinkTimeoutSec = 1
maxTcpPacketBytes = 65537 // 2 for size, then 2^16-1 for data.
)
type DriverStationConnection struct {
TeamId int
AllianceStation string
Auto bool
Enabled bool
EStop bool
AStop bool
DsLinked bool
RadioLinked bool
RioLinked bool
RobotLinked bool
BatteryVoltage float64
DsRobotTripTimeMs int
MissedPacketCount int
SecondsSinceLastRobotLink float64
lastPacketTime time.Time
lastRobotLinkedTime time.Time
packetCount int
tcpConn net.Conn
udpConn net.Conn
log *TeamMatchLog
SentGameData string
// WrongStation indicates if the team in the station is the incorrect team
// by being non-empty. If the team is in the correct station, or no team is
// connected, this is empty.
WrongStation string
}
var allianceStationPositionMap = map[string]byte{"R1": 0, "R2": 1, "R3": 2, "B1": 3, "B2": 4, "B3": 5}
func driverStationTeamIdFromRemoteAddr(addr net.Addr) (int, string, bool) {
ipAddress, _, err := net.SplitHostPort(addr.String())
if err != nil {
return 0, "", false
}
// Driver stations use team-specific 10.TE.AM.X addresses on a field network.
ipAddressBytes := net.ParseIP(ipAddress).To4()
if ipAddressBytes == nil || ipAddressBytes[0] != 10 {
return 0, ipAddress, false
}
return int(ipAddressBytes[1])*100 + int(ipAddressBytes[2]), ipAddress, true
}
// Opens a UDP connection for communicating to the driver station.
func newDriverStationConnection(
teamId int,
allianceStation string,
tcpConn net.Conn,
useLiteUdpPort bool,
) (*DriverStationConnection, error) {
ipAddress, _, err := net.SplitHostPort(tcpConn.RemoteAddr().String())
if err != nil {
return nil, err
}
log.Printf("Driver station for Team %d connected from %s\n", teamId, ipAddress)
udpSendPort := driverStationUdpSendPort
if useLiteUdpPort {
udpSendPort = driverStationUdpSendPortLite
}
udpConn, err := net.Dial("udp4", fmt.Sprintf("%s:%d", ipAddress, udpSendPort))
if err != nil {
return nil, err
}
return &DriverStationConnection{
TeamId: teamId,
AllianceStation: allianceStation,
tcpConn: tcpConn,
udpConn: udpConn,
}, nil
}
// Loops indefinitely to read packets and update connection status.
func (arena *Arena) listenForDsUdpPackets() {
udpAddress, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", network.ServerIpAddress, driverStationUdpReceivePort))
if err != nil {
log.Printf("Error resolving driver station UDP address: %v", err)
log.Printf("Change IP address to %s and restart Cheesy Arena to fix.", network.ServerIpAddress)
return
}
listener, err := net.ListenUDP("udp4", udpAddress)
if err != nil {
log.Fatalf("Error opening driver station UDP socket: %v", err)
}
log.Printf("Listening for driver stations on UDP port %d\n", driverStationUdpReceivePort)
defer listener.Close()
data := make([]byte, 1500)
for {
count, _ := listener.Read(data[:])
if count < 8 {
log.Printf("Received packet with insufficient length: %d", count)
continue
}
teamId := int(data[4])<<8 + int(data[5])
var dsConn *DriverStationConnection
for _, allianceStation := range arena.AllianceStations {
if allianceStation.Team != nil && allianceStation.Team.Id == teamId {
dsConn = allianceStation.DsConn
break
}
}
if dsConn != nil {
// Search through tags looking for tag 1
index := 8
for index < count {
length := data[index]
index++
if length == 0 {
continue
}
if index+int(length) > count {
log.Printf("Unable to finish parsing UDP packet")
break
}
tag := data[index]
if tag == 1 && length == 6 {
lost := (int(data[index+1]) << 8) + int(data[index+2])
ping := int(data[index+5])
dsConn.MissedPacketCount = lost
dsConn.DsRobotTripTimeMs = ping
}
index += int(length)
}
dsConn.DsLinked = true
dsConn.lastPacketTime = time.Now()
dsConn.RioLinked = data[3]&0x08 != 0
dsConn.RadioLinked = data[3]&0x10 != 0
dsConn.RobotLinked = data[3]&0x20 != 0
if dsConn.RobotLinked {
dsConn.lastRobotLinkedTime = time.Now()
// Robot battery voltage, stored as volts * 256.
dsConn.BatteryVoltage = float64(data[6]) + float64(data[7])/256
}
}
}
}
// Sends a control packet to the Driver Station and checks for timeout conditions.
func (dsConn *DriverStationConnection) update(arena *Arena, gameData string) error {
err := dsConn.sendControlPacket(arena, gameData)
if err != nil {
return err
}
if time.Since(dsConn.lastPacketTime).Seconds() > driverStationUdpLinkTimeoutSec {
dsConn.DsLinked = false
dsConn.RadioLinked = false
dsConn.RioLinked = false
dsConn.RobotLinked = false
dsConn.BatteryVoltage = 0
}
dsConn.SecondsSinceLastRobotLink = time.Since(dsConn.lastRobotLinkedTime).Seconds()
return nil
}
func (dsConn *DriverStationConnection) close() {
if dsConn.log != nil {
dsConn.log.Close()
}
if dsConn.udpConn != nil {
dsConn.udpConn.Close()
}
if dsConn.tcpConn != nil {
dsConn.tcpConn.Close()
}
}
// Called at the start of the match to allow for driver station initialization.
func (dsConn *DriverStationConnection) signalMatchStart(match *model.Match, wifiStatus *network.TeamWifiStatus) error {
// Zero out missed packet count and begin logging.
dsConn.MissedPacketCount = 0
var err error
dsConn.log, err = NewTeamMatchLog(dsConn.TeamId, match, wifiStatus)
return err
}
// Serializes the control information into a packet.
func (dsConn *DriverStationConnection) encodeControlPacket(arena *Arena) [22]byte {
var packet [22]byte
// Packet number, stored big-endian in two bytes.
packet[0] = byte((dsConn.packetCount >> 8) & 0xff)
packet[1] = byte(dsConn.packetCount & 0xff)
// Protocol version.
packet[2] = 0
// Robot status byte.
packet[3] = 0
if dsConn.Auto {
packet[3] |= 0x02
}
if dsConn.Enabled {
packet[3] |= 0x04
}
if dsConn.EStop {
packet[3] |= 0x80
}
if dsConn.AStop {
packet[3] |= 0x40
}
// Unknown or unused.
packet[4] = 0
// Alliance station.
packet[5] = allianceStationPositionMap[dsConn.AllianceStation]
// Match type.
match := arena.CurrentMatch
switch match.Type {
case model.Practice:
packet[6] = 1
case model.Qualification:
packet[6] = 2
case model.Playoff:
packet[6] = 3
default:
packet[6] = 0
}
// Match number.
packet[7] = byte(match.TypeOrder >> 8)
packet[8] = byte(match.TypeOrder & 0xff)
packet[9] = 1 // Match repeat number
// Current time.
currentTime := time.Now()
packet[10] = byte(((currentTime.Nanosecond() / 1000) >> 24) & 0xff)
packet[11] = byte(((currentTime.Nanosecond() / 1000) >> 16) & 0xff)
packet[12] = byte(((currentTime.Nanosecond() / 1000) >> 8) & 0xff)
packet[13] = byte((currentTime.Nanosecond() / 1000) & 0xff)
packet[14] = byte(currentTime.Second())
packet[15] = byte(currentTime.Minute())
packet[16] = byte(currentTime.Hour())
packet[17] = byte(currentTime.Day())
packet[18] = byte(currentTime.Month())
packet[19] = byte(currentTime.Year() - 1900)
// Remaining number of seconds in match.
var matchSecondsRemaining int
switch arena.MatchState {
case PreMatch, TimeoutActive, PostTimeout:
matchSecondsRemaining = game.MatchTiming.AutoDurationSec
case StartMatch, AutoPeriod:
matchSecondsRemaining = game.MatchTiming.AutoDurationSec - int(arena.MatchTimeSec())
case PausePeriod:
matchSecondsRemaining = game.MatchTiming.TeleopDurationSec
case TeleopPeriod:
matchSecondsRemaining = game.MatchTiming.AutoDurationSec + game.MatchTiming.TeleopDurationSec +
game.MatchTiming.PauseDurationSec - int(arena.MatchTimeSec())
default:
matchSecondsRemaining = 0
}
packet[20] = byte(matchSecondsRemaining >> 8 & 0xff)
packet[21] = byte(matchSecondsRemaining & 0xff)
// Increment the packet count for next time.
dsConn.packetCount++
return packet
}
// Builds and sends the next control packet to the Driver Station.
func (dsConn *DriverStationConnection) sendControlPacket(arena *Arena, gameData string) error {
gameDataErr := dsConn.checkGameData(gameData)
packet := dsConn.encodeControlPacket(arena)
if dsConn.udpConn != nil {
_, err := dsConn.udpConn.Write(packet[:])
if err != nil {
return err
}
}
return gameDataErr
}
// Listens for TCP connection requests to Cheesy Arena from driver stations.
func (arena *Arena) listenForDriverStations() {
l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", network.ServerIpAddress, driverStationTcpListenPort))
if err != nil {
log.Printf("Error opening driver station TCP socket: %v", err.Error())
log.Printf("Change IP address to %s and restart Cheesy Arena to fix.", network.ServerIpAddress)
return
}
defer l.Close()
arena.serveDriverStations(l)
}
func (arena *Arena) serveDriverStations(listener net.Listener) {
if tcpAddr, ok := listener.Addr().(*net.TCPAddr); ok {
log.Printf("Listening for driver stations on TCP port %d\n", tcpAddr.Port)
} else {
log.Printf("Listening for driver stations on TCP address %s\n", listener.Addr())
}
for {
tcpConn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
log.Println("Error accepting driver station connection: ", err.Error())
continue
}
// Read the team number back and start tracking the driver station.
var packet [5]byte
_, err = readTaggedTcpPacket(tcpConn, packet[:])
if err != nil {
log.Println("Error reading initial packet: ", err.Error())
continue
}
if !(packet[0] == 0 && packet[1] == 3 && packet[2] == 24) {
log.Printf("Invalid initial packet received: %v", packet)
tcpConn.Close()
continue
}
teamId := int(packet[3])<<8 + int(packet[4])
// Check to see if the team is supposed to be on the field, and notify the DS accordingly.
assignedStation := arena.getAssignedAllianceStation(teamId)
if assignedStation == "" {
log.Printf("Rejecting connection from Team %d, who is not in the current match, soon.", teamId)
go handleInvalidTcpConnection(tcpConn, 2, 0)
continue
}
// Read the team number from the IP address to check for a station mismatch.
stationStatus := byte(0)
wrongAssignedStation := ""
if arena.EventSettings.NetworkSecurityEnabled {
stationTeamId, ipAddress, ok := driverStationTeamIdFromRemoteAddr(tcpConn.RemoteAddr())
if ok && stationTeamId != teamId {
wrongAssignedStation = arena.getAssignedAllianceStation(stationTeamId)
// The team is supposed to be in this match, but is plugged into the wrong station.
if wrongAssignedStation != "" {
log.Printf("Team %d is in incorrect station %s.", teamId, wrongAssignedStation)
stationStatus = 1
} else {
log.Printf("Team %d is in unknown station with IP address %s.", teamId, ipAddress)
stationStatus = 1
}
}
}
var assignmentPacket [5]byte
assignmentPacket[0] = 0 // Packet size
assignmentPacket[1] = 3 // Packet size
assignmentPacket[2] = 25 // Packet type
log.Printf("Accepting connection from Team %d in station %s.", teamId, assignedStation)
assignmentPacket[3] = allianceStationPositionMap[assignedStation]
assignmentPacket[4] = stationStatus
_, err = tcpConn.Write(assignmentPacket[:])
if err != nil {
log.Printf("Error sending driver station assignment packet: %v", err)
tcpConn.Close()
continue
}
// Write event code here. We need to strip any numbers off the front if it has it.
// We also need to limit to 62 characters.
eventName := arena.EventSettings.TbaEventCode
if len(eventName) > 0 {
trimIndex := 0
for trimIndex < len(eventName) && eventName[trimIndex] >= '0' && eventName[trimIndex] <= '9' {
trimIndex++
}
eventName = eventName[trimIndex:]
if len(eventName) > 62 {
eventName = eventName[:62]
}
if len(eventName) > 0 {
eventNamePacket := make([]byte, 4+len(eventName))
eventNamePacket[0] = 0
eventNamePacket[1] = byte(len(eventName) + 2)
eventNamePacket[2] = 20 // Packet type for event name
eventNamePacket[3] = byte(len(eventName))
copy(eventNamePacket[4:], []byte(eventName))
_, err = tcpConn.Write(eventNamePacket)
if err != nil {
log.Printf("Error sending event name packet: %v", err)
tcpConn.Close()
continue
}
}
}
dsConn, err := newDriverStationConnection(teamId, assignedStation, tcpConn, arena.EventSettings.UseLiteUdpPort)
if err != nil {
log.Printf("Error registering driver station connection: %v", err)
tcpConn.Close()
continue
}
arena.AllianceStations[assignedStation].DsConn = dsConn
if wrongAssignedStation != "" {
dsConn.WrongStation = wrongAssignedStation
}
// Spin up a goroutine to handle further TCP communication with this driver station.
go dsConn.handleTcpConnection(arena)
}
}
func readTaggedTcpPacket(tcpConn net.Conn, buffer []byte) (int, error) {
if len(buffer) < 2 {
return 0, fmt.Errorf("buffer too small to read TCP packet")
}
tcpConn.SetReadDeadline(time.Now().Add(time.Second * driverStationTcpLinkTimeoutSec))
_, err := io.ReadFull(tcpConn, buffer[:2])
if err != nil {
return 0, err
}
packetLength := int(buffer[0])<<8 + int(buffer[1])
if len(buffer) < 2+packetLength {
return 0, fmt.Errorf("buffer too small to read full TCP packet")
}
_, err = io.ReadFull(tcpConn, buffer[2:2+packetLength])
if err != nil {
return 0, err
}
return 2 + packetLength, nil
}
func (dsConn *DriverStationConnection) handleTcpConnection(arena *Arena) {
buffer := make([]byte, maxTcpPacketBytes)
for {
_, err := readTaggedTcpPacket(dsConn.tcpConn, buffer)
if err != nil {
log.Printf("Error reading from connection for Team %d: %v", dsConn.TeamId, err)
dsConn.close()
if arena.AllianceStations[dsConn.AllianceStation].DsConn == dsConn {
arena.AllianceStations[dsConn.AllianceStation].DsConn = nil
}
break
}
packetType := int(buffer[2])
switch packetType {
case 29:
// DS keepalive packet; do nothing.
continue
case 22:
// Robot log packet. Just use to trigger fms log
// Create a log entry if the match is in progress.
matchTimeSec := arena.MatchTimeSec()
if matchTimeSec > 0 && dsConn.log != nil {
dsConn.log.LogDsPacket(matchTimeSec, packetType, dsConn)
}
default:
log.Printf("Received unknown packet type %d from Team %d", packetType, dsConn.TeamId)
}
}
}
func handleInvalidTcpConnection(tcpConn net.Conn, status int, station int) {
log.Printf("Handling invalid TCP connection from %v with status %d and station %d", tcpConn.RemoteAddr(), status, station)
var assignmentPacket [5]byte
assignmentPacket[0] = 0 // Packet size
assignmentPacket[1] = 3 // Packet size
assignmentPacket[2] = 25 // Packet type
assignmentPacket[3] = byte(station)
assignmentPacket[4] = byte(status)
_, err := tcpConn.Write(assignmentPacket[:])
if err != nil {
log.Printf("Error sending invalid driver station assignment packet: %v", err)
tcpConn.Close()
return
}
buffer := make([]byte, maxTcpPacketBytes)
for {
_, err := readTaggedTcpPacket(tcpConn, buffer)
if err != nil {
log.Printf("Error reading from connection for invalid driver station: %v", err)
break
}
}
tcpConn.Close()
}
func (dsConn *DriverStationConnection) checkGameData(gameData string) error {
needsGameDataUpdate := dsConn.SentGameData != gameData
if needsGameDataUpdate {
err := dsConn.sendGameDataPacket(gameData)
if err != nil {
log.Printf("Error sending game data packet to Team %d: %v", dsConn.TeamId, err)
return err
} else {
dsConn.SentGameData = gameData
}
}
return nil
}
// Sends a TCP packet containing the given game data to the driver station.
func (dsConn *DriverStationConnection) sendGameDataPacket(gameData string) error {
byteData := []byte(gameData)
size := len(byteData)
packet := make([]byte, size+4)
packet[0] = 0 // Packet size
packet[1] = byte(size + 2) // Packet size
packet[2] = 28 // Packet type
packet[3] = byte(size) // Data size
// Fill the rest of the packet with the data.
for i, character := range byteData {
packet[i+4] = character
}
if dsConn.tcpConn != nil {
_, err := dsConn.tcpConn.Write(packet)
return err
}
return nil
}
================================================
FILE: field/driver_station_connection_test.go
================================================
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package field
import (
"fmt"
"net"
"testing"
"time"
"github.com/Team254/cheesy-arena/model"
"github.com/stretchr/testify/assert"
)
func TestEncodeControlPacket(t *testing.T) {
arena := setupTestArena(t)
tcpConn := setupFakeTcpConnection(t)
defer tcpConn.Close()
dsConn, err := newDriverStationConnection(254, "R1", tcpConn, false)
assert.Nil(t, err)
defer dsConn.close()
data := dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(0), data[5])
assert.Equal(t, byte(0), data[6])
assert.Equal(t, byte(0), data[20])
assert.Equal(t, byte(15), data[21])
// Check the different alliance station values.
dsConn.AllianceStation = "R2"
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(1), data[5])
dsConn.AllianceStation = "R3"
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(2), data[5])
dsConn.AllianceStation = "B1"
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(3), data[5])
dsConn.AllianceStation = "B2"
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(4), data[5])
dsConn.AllianceStation = "B3"
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(5), data[5])
// Check packet count rollover.
dsConn.packetCount = 255
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(0), data[0])
assert.Equal(t, byte(255), data[1])
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(1), data[0])
assert.Equal(t, byte(0), data[1])
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(1), data[0])
assert.Equal(t, byte(1), data[1])
dsConn.packetCount = 65535
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(255), data[0])
assert.Equal(t, byte(255), data[1])
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(0), data[0])
assert.Equal(t, byte(0), data[1])
// Check different robot statuses.
dsConn.Auto = true
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(2), data[3])
dsConn.Enabled = true
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(6), data[3])
dsConn.Auto = false
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(4), data[3])
dsConn.EStop = true
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(132), data[3])
dsConn.AStop = true
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(196), data[3])
// Check different match types.
arena.CurrentMatch.Type = model.Practice
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(1), data[6])
arena.CurrentMatch.Type = model.Qualification
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(2), data[6])
arena.CurrentMatch.Type = model.Playoff
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(3), data[6])
// Check match numbers.
arena.CurrentMatch.Type = model.Practice
arena.CurrentMatch.TypeOrder = 42
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(0), data[7])
assert.Equal(t, byte(42), data[8])
arena.CurrentMatch.Type = model.Qualification
arena.CurrentMatch.TypeOrder = 258
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(1), data[7])
assert.Equal(t, byte(2), data[8])
arena.CurrentMatch.Type = model.Playoff
arena.CurrentMatch.TypeOrder = 13
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(0), data[7])
assert.Equal(t, byte(13), data[8])
// Check the countdown at different points during the match.
arena.MatchState = AutoPeriod
arena.MatchStartTime = time.Now().Add(-time.Duration(4 * time.Second))
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(11), data[21])
arena.MatchState = PausePeriod
arena.MatchStartTime = time.Now().Add(-time.Duration(16 * time.Second))
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(135), data[21])
arena.MatchState = TeleopPeriod
arena.MatchStartTime = time.Now().Add(-time.Duration(33 * time.Second))
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(119), data[21])
arena.MatchStartTime = time.Now().Add(-time.Duration(150 * time.Second))
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(2), data[21])
arena.MatchState = PostMatch
arena.MatchStartTime = time.Now().Add(-time.Duration(180 * time.Second))
data = dsConn.encodeControlPacket(arena)
assert.Equal(t, byte(0), data[21])
}
func TestSendControlPacket(t *testing.T) {
arena := setupTestArena(t)
tcpConn := setupFakeTcpConnection(t)
defer tcpConn.Close()
dsConn, err := newDriverStationConnection(254, "R1", tcpConn, false)
assert.Nil(t, err)
defer dsConn.close()
// No real way of checking this since the destination IP is remote, so settle for there being no errors.
err = dsConn.sendControlPacket(arena, "")
assert.Nil(t, err)
}
func TestListenForDriverStations(t *testing.T) {
arena := setupTestArena(t)
serverAddress := startTestDriverStationServer(t, arena)
// Connect with an invalid initial packet.
tcpConn, err := net.Dial("tcp", serverAddress)
if assert.Nil(t, err) {
dataSend := [5]byte{0, 3, 29, 0, 0}
tcpConn.Write(dataSend[:])
var dataReceived [100]byte
_, err = readTaggedTcpPacket(tcpConn, dataReceived[:])
assert.NotNil(t, err)
tcpConn.Close()
}
// Connect as a team not in the current match.
tcpConn, err = net.Dial("tcp", serverAddress)
if assert.Nil(t, err) {
dataSend := [5]byte{0, 3, 24, 5, 223}
tcpConn.Write(dataSend[:])
var dataReceived [5]byte
count, err := readTaggedTcpPacket(tcpConn, dataReceived[:])
assert.Nil(t, err)
assert.Equal(t, count, 5)
assert.Equal(t, [5]byte{0, 3, 25, 0, 2}, dataReceived)
tcpConn.Close()
}
// Connect as a team in the current match.
arena.assignTeam(1503, "B2")
// Connect as a team in the current match with a fragmented initial packet.
tcpConn, err = net.Dial("tcp", serverAddress)
if assert.Nil(t, err) {
defer tcpConn.Close()
dataSend := [5]byte{0, 3, 24, 5, 223}
tcpConn.Write(dataSend[:1])
tcpConn.Write(dataSend[1:5])
var dataReceived [5]byte
count, err := readTaggedTcpPacket(tcpConn, dataReceived[:])
assert.Nil(t, err)
assert.Equal(t, count, 5)
}
// Set event name
arena.EventSettings.TbaEventCode = "2026CC"
tcpConn, err = net.Dial("tcp", serverAddress)
if assert.Nil(t, err) {
defer tcpConn.Close()
dataSend := [5]byte{0, 3, 24, 5, 223}
tcpConn.Write(dataSend[:])
var dataReceived [100]byte
_, err := readTaggedTcpPacket(tcpConn, dataReceived[:])
assert.Nil(t, err)
// Read event name
count, err := readTaggedTcpPacket(tcpConn, dataReceived[:])
assert.Nil(t, err)
assert.Equal(t, count, 6)
assert.Equal(t, []byte{0, 4, 20, 2, 67, 67}, dataReceived[:6])
}
tcpConn, err = net.Dial("tcp", serverAddress)
if assert.Nil(t, err) {
defer tcpConn.Close()
dataSend := [5]byte{0, 3, 24, 5, 223}
tcpConn.Write(dataSend[:])
var dataReceived [5]byte
_, err = readTaggedTcpPacket(tcpConn, dataReceived[:])
assert.Nil(t, err)
assert.Equal(t, [5]byte{0, 3, 25, 4, 0}, dataReceived)
dsConn := waitForDriverStationConnection(t, arena, "B2")
if assert.NotNil(t, dsConn) {
assert.Equal(t, 1503, dsConn.TeamId)
assert.Equal(t, "B2", dsConn.AllianceStation)
// Check that an unknown packet type gets ignored and a status packet gets decoded.
dataSend = [5]byte{0, 3, 37, 0, 0}
tcpConn.Write(dataSend[:])
}
}
}
func TestListenForDriverStations_NetworkSecurityIgnoresNonFieldIp(t *testing.T) {
arena := setupTestArena(t)
arena.EventSettings.NetworkSecurityEnabled = true
arena.assignTeam(1503, "B2")
serverAddress := startTestDriverStationServer(t, arena)
tcpConn, err := net.Dial("tcp", serverAddress)
if assert.Nil(t, err) {
defer tcpConn.Close()
dataSend := [5]byte{0, 3, 24, 5, 223}
tcpConn.Write(dataSend[:])
var dataReceived [5]byte
_, err = readTaggedTcpPacket(tcpConn, dataReceived[:])
assert.Nil(t, err)
assert.Equal(t, [5]byte{0, 3, 25, 4, 0}, dataReceived)
dsConn := waitForDriverStationConnection(t, arena, "B2")
if assert.NotNil(t, dsConn) {
assert.Equal(t, "", dsConn.WrongStation)
}
}
}
func TestNewDriverStationConnection_UdpPortSelection(t *testing.T) {
tcpConn := setupFakeTcpConnection(t)
defer tcpConn.Close()
// Test with default settings (FMS port).
dsConn, err := newDriverStationConnection(254, "R1", tcpConn, false)
assert.Nil(t, err)
defer dsConn.close()
assert.Contains(t, dsConn.udpConn.RemoteAddr().String(), fmt.Sprintf(":%d", driverStationUdpSendPort))
tcpConnLite := setupFakeTcpConnection(t)
defer tcpConnLite.Close()
// Test with FMS Lite port enabled.
dsConnLite, err := newDriverStationConnection(254, "R1", tcpConnLite, true)
assert.Nil(t, err)
defer dsConnLite.close()
assert.Contains(t, dsConnLite.udpConn.RemoteAddr().String(), fmt.Sprintf(":%d", driverStationUdpSendPortLite))
}
func setupFakeTcpConnection(t *testing.T) net.Conn {
// Set up a fake TCP endpoint and connection to it.
l, err := net.Listen("tcp", "127.0.0.1:0")
assert.Nil(t, err)
defer l.Close()
tcpConn, err := net.Dial("tcp", l.Addr().String())
assert.Nil(t, err)
return tcpConn
}
func startTestDriverStationServer(t *testing.T, arena *Arena) string {
listener, err := net.Listen("tcp", "127.0.0.1:0")
assert.Nil(t, err)
t.Cleanup(func() {
listener.Close()
})
go arena.serveDriverStations(listener)
return listener.Addr().String()
}
func waitForDriverStationConnection(t *testing.T, arena *Arena, station string) *DriverStationConnection {
t.Helper()
var dsConn *DriverStationConnection
if !assert.Eventually(t, func() bool {
dsConn = arena.AllianceStations[station].DsConn
return dsConn != nil
}, time.Second, 10*time.Millisecond) {
return nil
}
return dsConn
}
================================================
FILE: field/event_status.go
================================================
// Copyright 2020 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model and functions for reporting on event status.
package field
import (
"fmt"
"github.com/Team254/cheesy-arena/model"
"math"
"time"
)
const maxExpectedCycleTimeSec = 900
type EventStatus struct {
CycleTime string
EarlyLateMessage string
lastMatchStartTime time.Time
lastMatchScheduledStartTime time.Time
}
// Calculates the last cycle time and publishes an update to the displays that show it.
func (arena *Arena) updateCycleTime(matchStartTime time.Time) {
expectedCycleTimeSec := arena.CurrentMatch.Time.Sub(arena.EventStatus.lastMatchScheduledStartTime).Seconds()
if arena.EventStatus.lastMatchStartTime.IsZero() || expectedCycleTimeSec > maxExpectedCycleTimeSec ||
arena.CurrentMatch.Type == model.Test {
// We don't know when the previous match was started or there was a big gap that we shouldn't count.
arena.EventStatus.CycleTime = ""
} else {
cycleTimeSec := int(matchStartTime.Sub(arena.EventStatus.lastMatchStartTime).Seconds())
hours := cycleTimeSec / 3600
minutes := cycleTimeSec % 3600 / 60
seconds := cycleTimeSec % 60
if hours > 0 {
arena.EventStatus.CycleTime = fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
} else {
arena.EventStatus.CycleTime = fmt.Sprintf("%d:%02d", minutes, seconds)
}
deltaSec := cycleTimeSec - int(expectedCycleTimeSec)
var direction string
if deltaSec > 0 {
direction = "slower"
} else {
direction = "faster"
deltaSec = -deltaSec
}
arena.EventStatus.CycleTime += fmt.Sprintf(
" (%d:%02d %s than scheduled)", deltaSec/60, deltaSec%60, direction,
)
}
arena.EventStatus.lastMatchStartTime = matchStartTime
arena.EventStatus.lastMatchScheduledStartTime = arena.CurrentMatch.Time
arena.EventStatusNotifier.Notify()
}
// Checks how early or late the event is running and publishes an update to the displays that show it.
func (arena *Arena) updateEarlyLateMessage() {
newEarlyLateMessage := arena.getEarlyLateMessage()
if newEarlyLateMessage != arena.EventStatus.EarlyLateMessage {
arena.EventStatus.EarlyLateMessage = newEarlyLateMessage
arena.EventStatusNotifier.Notify()
}
}
// Updates the string that indicates how early or late the event is running.
func (arena *Arena) getEarlyLateMessage() string {
currentMatch := arena.CurrentMatch
if currentMatch.Type == model.Test {
return ""
}
if currentMatch.IsComplete() {
// This is a replay or otherwise unpredictable situation.
return ""
}
var minutesLate float64
if arena.MatchState > PreMatch && arena.MatchState < PostMatch {
// The match is in progress; simply calculate lateness from its start time.
minutesLate = currentMatch.StartedAt.Sub(currentMatch.Time).Minutes()
} else {
// We need to check the adjacent matches to accurately determine lateness.
matches, _ := arena.Database.GetMatchesByType(currentMatch.Type, false)
previousMatchIndex := -1
nextMatchIndex := len(matches)
for i, match := range matches {
if match.Id == currentMatch.Id {
previousMatchIndex = i - 1
nextMatchIndex = i + 1
break
}
}
if arena.MatchState == PreMatch || arena.MatchState == TimeoutActive || arena.MatchState == PostTimeout {
currentMinutesLate := time.Now().Sub(currentMatch.Time).Minutes()
if previousMatchIndex >= 0 &&
currentMatch.Time.Sub(matches[previousMatchIndex].Time).Minutes() <= MaxMatchGapMin {
previousMatch := matches[previousMatchIndex]
previousMinutesLate := previousMatch.StartedAt.Sub(previousMatch.Time).Minutes()
minutesLate = math.Max(previousMinutesLate, currentMinutesLate)
} else {
minutesLate = math.Max(currentMinutesLate, 0)
}
} else if arena.MatchState == PostMatch {
currentMinutesLate := currentMatch.StartedAt.Sub(currentMatch.Time).Minutes()
if nextMatchIndex < len(matches) {
nextMatch := matches[nextMatchIndex]
nextMinutesLate := time.Now().Sub(nextMatch.Time).Minutes()
minutesLate = math.Max(currentMinutesLate, nextMinutesLate)
} else {
minutesLate = currentMinutesLate
}
}
}
if minutesLate > earlyLateThresholdMin {
return fmt.Sprintf("Event is running %d minutes late", int(minutesLate))
} else if minutesLate < -earlyLateThresholdMin {
return fmt.Sprintf("Event is running %d minutes early", int(-minutesLate))
}
return "Event is running on schedule"
}
================================================
FILE: field/event_status_test.go
================================================
// Copyright 2020 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package field
import (
"github.com/Team254/cheesy-arena/game"
"github.com/Team254/cheesy-arena/model"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestCycleTime(t *testing.T) {
arena := setupTestArena(t)
arena.CurrentMatch.Type = model.Practice
assert.Equal(t, "", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Time{})
assert.Equal(t, "", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now().Add(-125 * time.Second))
assert.Equal(t, "", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now())
assert.Regexp(t, "2:05.*", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now().Add(3456 * time.Second))
assert.Regexp(t, "57:36.*", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now().Add(5 * time.Hour))
assert.Regexp(t, "4:02:24.*", arena.EventStatus.CycleTime)
arena.updateCycleTime(time.Now().Add(123*time.Hour + 1256*time.Second))
assert.Regexp(t, "118:20:56.*", arena.EventStatus.CycleTime)
// Cycle time should be suppressed for test matches.
arena.CurrentMatch.Type = model.Test
arena.updateCycleTime(time.Now().Add(123*time.Hour + 1256*time.Second))
assert.Regexp(t, "", arena.EventStatus.CycleTime)
}
func TestCycleTimeDelta(t *testing.T) {
arena := setupTestArena(t)
arena.CurrentMatch.Type = model.Practice
// Check perfect cycle time.
arena.CurrentMatch.Time = time.Unix(1000, 0)
arena.updateCycleTime(time.Unix(1000, 0))
assert.Equal(t, "", arena.EventStatus.CycleTime)
arena.CurrentMatch.Time = time.Unix(1754, 0)
arena.updateCycleTime(time.Unix(1754, 0))
assert.Equal(t, "12:34 (0:00 faster than scheduled)", arena.EventStatus.CycleTime)
// Check faster cycle time.
arena.CurrentMatch.Time = time.Unix(1000, 0)
arena.updateCycleTime(time.Unix(1000, 0))
arena.CurrentMatch.Time = time.Unix(1500, 0)
arena.updateCycleTime(time.Unix(1417, 0))
assert.Equal(t, "6:57 (1:23 faster than scheduled)", arena.EventStatus.CycleTime)
// Check slower cycle time.
arena.CurrentMatch.Time = time.Unix(1000, 0)
arena.updateCycleTime(time.Unix(1000, 0))
arena.CurrentMatch.Time = time.Unix(1500, 0)
arena.updateCycleTime(time.Unix(2500, 0))
assert.Equal(t, "25:00 (16:40 slower than scheduled)", arena.EventStatus.CycleTime)
// Check over a long gap in the schedule.
arena.CurrentMatch.Time = time.Unix(1000, 0)
arena.updateCycleTime(time.Unix(1000, 0))
arena.CurrentMatch.Time = time.Unix(2000, 0)
arena.updateCycleTime(time.Unix(2000, 0))
assert.Equal(t, "", arena.EventStatus.CycleTime)
}
func TestEarlyLateMessage(t *testing.T) {
arena := setupTestArena(t)
arena.LoadTestMatch()
assert.Equal(t, "", arena.getEarlyLateMessage())
arena.Database.CreateMatch(&model.Match{Type: model.Qualification, TypeOrder: 1})
arena.Database.CreateMatch(&model.Match{Type: model.Qualification, TypeOrder: 2})
matches, _ := arena.Database.GetMatchesByType(model.Qualification, false)
assert.Equal(t, 2, len(matches))
setMatch(arena.Database, &matches[0], time.Now().Add(300*time.Second), time.Time{}, false)
arena.CurrentMatch = &matches[0]
arena.MatchState = PreMatch
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-120*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-180*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes late", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(181*time.Second), time.Now(), false)
arena.MatchState = AutoPeriod
assert.Equal(t, "Event is running 3 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-300*time.Second), time.Now().Add(-601*time.Second), false)
setMatch(arena.Database, &matches[1], time.Now().Add(481*time.Second), time.Time{}, false)
arena.MatchState = PostMatch
assert.Equal(t, "Event is running 5 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(181*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes late", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[0], time.Now().Add(-300*time.Second), time.Now().Add(-601*time.Second), true)
assert.Equal(t, "", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(901*time.Second), time.Time{}, false)
arena.CurrentMatch = &matches[1]
arena.MatchState = PreMatch
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(899*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 5 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(60*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-120*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running on schedule", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Time{}, false)
assert.Equal(t, "Event is running 3 minutes late", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now().Add(-180*time.Second), time.Now().Add(-541*time.Second), false)
arena.MatchState = TeleopPeriod
assert.Equal(t, "Event is running 6 minutes early", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now(), time.Now().Add(481*time.Second), false)
arena.MatchState = PostMatch
assert.Equal(t, "Event is running 8 minutes late", arena.getEarlyLateMessage())
setMatch(arena.Database, &matches[1], time.Now(), time.Now().Add(481*time.Second), true)
assert.Equal(t, "", arena.getEarlyLateMessage())
// Check other match types.
arena.MatchState = PreMatch
arena.CurrentMatch = &model.Match{Type: model.Practice, Time: time.Now().Add(-181 * time.Second)}
assert.Equal(t, "Event is running 3 minutes late", arena.getEarlyLateMessage())
arena.CurrentMatch = &model.Match{Type: model.Playoff, Time: time.Now().Add(-181 * time.Second)}
assert.Equal(t, "Event is running 3 minutes late", arena.getEarlyLateMessage())
}
func setMatch(database *model.Database, match *model.Match, matchTime time.Time, startedAt time.Time, isComplete bool) {
match.Time = matchTime
match.StartedAt = startedAt
if isComplete {
match.Status = game.TieMatch
} else {
match.Status = game.MatchScheduled
}
_ = database.UpdateMatch(match)
}
================================================
FILE: field/fake_plc_test.go
================================================
// Copyright 2023 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Contains a fake implementation of the PLC interface for testing.
package field
import (
"github.com/Team254/cheesy-arena/websocket"
)
type FakePlc struct {
isEnabled bool
fieldEStop bool
redEStops [3]bool
blueEStops [3]bool
redAStops [3]bool
blueAStops [3]bool
redEthernetConnected [3]bool
blueEthernetConnected [3]bool
stackLights [4]bool
stackLightBuzzer bool
fieldResetLight bool
cycleState bool
redProcessorCount int
blueProcessorCount int
redTrussLights [3]bool
blueTrussLights [3]bool
}
func (plc *FakePlc) SetAddress(address string) {
}
func (plc *FakePlc) IsEnabled() bool {
return plc.isEnabled
}
func (plc *FakePlc) IsHealthy() bool {
return true
}
func (plc *FakePlc) IoChangeNotifier() *websocket.Notifier {
return nil
}
func (plc *FakePlc) Run() {
}
func (plc *FakePlc) GetArmorBlockStatuses() map[string]bool {
return map[string]bool{}
}
func (plc *FakePlc) GetFieldEStop() bool {
return plc.fieldEStop
}
func (plc *FakePlc) GetTeamEStops() ([3]bool, [3]bool) {
return plc.redEStops, plc.blueEStops
}
func (plc *FakePlc) GetTeamAStops() ([3]bool, [3]bool) {
return plc.redAStops, plc.blueAStops
}
func (plc *FakePlc) GetEthernetConnected() ([3]bool, [3]bool) {
return plc.redEthernetConnected, plc.blueEthernetConnected
}
func (plc *FakePlc) ResetMatch() {
}
func (plc *FakePlc) SetStackLights(red, blue, orange, green bool) {
plc.stackLights[0] = red
plc.stackLights[1] = blue
plc.stackLights[2] = orange
plc.stackLights[3] = green
}
func (plc *FakePlc) SetStackBuzzer(state bool) {
plc.stackLightBuzzer = state
}
func (plc *FakePlc) SetFieldResetLight(state bool) {
plc.fieldResetLight = state
}
func (plc *FakePlc) GetCycleState(max, index, duration int) bool {
return plc.cycleState
}
func (plc *FakePlc) GetInputNames() []string {
return []string{}
}
func (plc *FakePlc) GetRegisterNames() []string {
return []string{}
}
func (plc *FakePlc) GetCoilNames() []string {
return []string{}
}
func (plc *FakePlc) GetProcessorCounts() (int, int) {
return plc.redProcessorCount, plc.blueProcessorCount
}
func (plc *FakePlc) SetTrussLights(redLights, blueLights [3]bool) {
plc.redTrussLights = redLights
plc.blueTrussLights = blueLights
}
================================================
FILE: field/realtime_score.go
================================================
// Copyright 2017 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model representing the current state of the score during a match.
package field
import "github.com/Team254/cheesy-arena/game"
type RealtimeScore struct {
CurrentScore game.Score
Cards map[string]string
FoulsCommitted bool
}
func NewRealtimeScore() *RealtimeScore {
return &RealtimeScore{Cards: make(map[string]string)}
}
================================================
FILE: field/scoring_panel_registry.go
================================================
// Copyright 2019 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model representing and methods for tracking the state of a realtime scoring panel.
package field
import (
"github.com/Team254/cheesy-arena/websocket"
"sync"
)
type ScoringPanelRegistry struct {
scoringPanels map[string]map[*websocket.Websocket]bool // The score committed state for each panel.
mutex sync.Mutex
}
func (registry *ScoringPanelRegistry) initialize() {
registry.scoringPanels = map[string]map[*websocket.Websocket]bool{}
}
// Resets the score committed state for each registered panel to false.
func (registry *ScoringPanelRegistry) resetScoreCommitted() {
registry.mutex.Lock()
defer registry.mutex.Unlock()
for _, panels := range registry.scoringPanels {
for key := range panels {
panels[key] = false
}
}
}
// Returns the number of registered panels for the given position.
func (registry *ScoringPanelRegistry) GetNumPanels(position string) int {
registry.mutex.Lock()
defer registry.mutex.Unlock()
return len(registry.scoringPanels[position])
}
// Returns the number of registered panels whose score is committed for the given position.
func (registry *ScoringPanelRegistry) GetNumScoreCommitted(position string) int {
registry.mutex.Lock()
defer registry.mutex.Unlock()
numCommitted := 0
for _, panel := range registry.scoringPanels[position] {
if panel {
numCommitted++
}
}
return numCommitted
}
// Adds a panel to the registry, referenced by its websocket pointer.
func (registry *ScoringPanelRegistry) RegisterPanel(position string, ws *websocket.Websocket) {
registry.mutex.Lock()
defer registry.mutex.Unlock()
if registry.scoringPanels[position] == nil {
registry.scoringPanels[position] = make(map[*websocket.Websocket]bool)
}
registry.scoringPanels[position][ws] = false
}
// Sets the score committed state to true for the given panel, referenced by its websocket pointer.
func (registry *ScoringPanelRegistry) SetScoreCommitted(position string, ws *websocket.Websocket) {
registry.mutex.Lock()
defer registry.mutex.Unlock()
registry.scoringPanels[position][ws] = true
}
// Removes a panel from the registry, referen
gitextract_jrfey4xk/
├── .editorconfig
├── .github/
│ └── workflows/
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── AGENTS.md
├── LICENSE
├── README.md
├── coverage
├── field/
│ ├── arena.go
│ ├── arena_notifiers.go
│ ├── arena_test.go
│ ├── display.go
│ ├── display_test.go
│ ├── driver_station_connection.go
│ ├── driver_station_connection_test.go
│ ├── event_status.go
│ ├── event_status_test.go
│ ├── fake_plc_test.go
│ ├── realtime_score.go
│ ├── scoring_panel_registry.go
│ ├── scoring_panel_registry_test.go
│ ├── team_match_log.go
│ ├── team_sign.go
│ ├── team_sign_test.go
│ └── test_helpers.go
├── fix_avatar_colors_for_overlay
├── font/
│ ├── helvetica.json
│ ├── helveticab.json
│ ├── helveticabi.json
│ └── helveticai.json
├── game/
│ ├── foul.go
│ ├── match_sounds.go
│ ├── match_timing.go
│ ├── ranking_fields.go
│ ├── ranking_fields_test.go
│ ├── reef.go
│ ├── reef_test.go
│ ├── rule.go
│ ├── rule_test.go
│ ├── score.go
│ ├── score_summary.go
│ ├── score_summary_test.go
│ ├── score_test.go
│ └── test_helpers.go
├── go.mod
├── go.sum
├── main.go
├── model/
│ ├── alliance.go
│ ├── alliance_test.go
│ ├── award.go
│ ├── award_test.go
│ ├── database.go
│ ├── database_test.go
│ ├── event_settings.go
│ ├── event_settings_test.go
│ ├── judging_slot.go
│ ├── judging_slot_test.go
│ ├── lower_third.go
│ ├── lower_third_test.go
│ ├── match.go
│ ├── match_result.go
│ ├── match_result_test.go
│ ├── match_test.go
│ ├── matchtype_string.go
│ ├── ranking.go
│ ├── ranking_test.go
│ ├── schedule_block.go
│ ├── schedule_block_test.go
│ ├── scheduled_break.go
│ ├── scheduled_break_test.go
│ ├── sponsor_slide.go
│ ├── sponsor_slide_test.go
│ ├── table.go
│ ├── table_test.go
│ ├── team.go
│ ├── team_test.go
│ ├── test_helpers.go
│ ├── user_session.go
│ └── user_session_test.go
├── network/
│ ├── access_point.go
│ ├── access_point_test.go
│ ├── sccswitch.go
│ ├── sccswitch_test.go
│ ├── switch.go
│ ├── switch_test.go
│ └── testdata/
│ ├── iwinfo_0_teams.txt
│ ├── iwinfo_2_teams.txt
│ ├── iwinfo_6_teams.txt
│ └── iwinfo_invalid.txt
├── partner/
│ ├── blackmagic.go
│ ├── blackmagic_test.go
│ ├── companion.go
│ ├── companion_test.go
│ ├── nexus.go
│ ├── nexus_test.go
│ ├── tba.go
│ └── tba_test.go
├── playoff/
│ ├── alliance_source.go
│ ├── break_spec.go
│ ├── double_elimination.go
│ ├── double_elimination_test.go
│ ├── match_group.go
│ ├── match_group_test.go
│ ├── matchup.go
│ ├── matchup_test.go
│ ├── playoff_match_result.go
│ ├── playoff_tournament.go
│ ├── playoff_tournament_test.go
│ ├── single_elimination.go
│ ├── single_elimination_test.go
│ └── test_helpers.go
├── plc/
│ ├── armorblock_string.go
│ ├── coil_string.go
│ ├── fake_modbus_client_test.go
│ ├── input_string.go
│ ├── plc.go
│ ├── plc_test.go
│ └── register_string.go
├── schedules/
│ ├── 100_1.csv
│ ├── 100_10.csv
│ ├── 100_11.csv
│ ├── 100_12.csv
│ ├── 100_13.csv
│ ├── 100_14.csv
│ ├── 100_2.csv
│ ├── 100_3.csv
│ ├── 100_4.csv
│ ├── 100_5.csv
│ ├── 100_6.csv
│ ├── 100_7.csv
│ ├── 100_8.csv
│ ├── 100_9.csv
│ ├── 10_1.csv
│ ├── 10_10.csv
│ ├── 10_11.csv
│ ├── 10_12.csv
│ ├── 10_13.csv
│ ├── 10_14.csv
│ ├── 10_2.csv
│ ├── 10_3.csv
│ ├── 10_4.csv
│ ├── 10_5.csv
│ ├── 10_6.csv
│ ├── 10_7.csv
│ ├── 10_8.csv
│ ├── 10_9.csv
│ ├── 11_1.csv
│ ├── 11_10.csv
│ ├── 11_11.csv
│ ├── 11_12.csv
│ ├── 11_13.csv
│ ├── 11_14.csv
│ ├── 11_2.csv
│ ├── 11_3.csv
│ ├── 11_4.csv
│ ├── 11_5.csv
│ ├── 11_6.csv
│ ├── 11_7.csv
│ ├── 11_8.csv
│ ├── 11_9.csv
│ ├── 12_1.csv
│ ├── 12_10.csv
│ ├── 12_11.csv
│ ├── 12_12.csv
│ ├── 12_13.csv
│ ├── 12_14.csv
│ ├── 12_2.csv
│ ├── 12_3.csv
│ ├── 12_4.csv
│ ├── 12_5.csv
│ ├── 12_6.csv
│ ├── 12_7.csv
│ ├── 12_8.csv
│ ├── 12_9.csv
│ ├── 13_1.csv
│ ├── 13_10.csv
│ ├── 13_11.csv
│ ├── 13_12.csv
│ ├── 13_13.csv
│ ├── 13_14.csv
│ ├── 13_2.csv
│ ├── 13_3.csv
│ ├── 13_4.csv
│ ├── 13_5.csv
│ ├── 13_6.csv
│ ├── 13_7.csv
│ ├── 13_8.csv
│ ├── 13_9.csv
│ ├── 14_1.csv
│ ├── 14_10.csv
│ ├── 14_11.csv
│ ├── 14_12.csv
│ ├── 14_13.csv
│ ├── 14_14.csv
│ ├── 14_2.csv
│ ├── 14_3.csv
│ ├── 14_4.csv
│ ├── 14_5.csv
│ ├── 14_6.csv
│ ├── 14_7.csv
│ ├── 14_8.csv
│ ├── 14_9.csv
│ ├── 15_1.csv
│ ├── 15_10.csv
│ ├── 15_11.csv
│ ├── 15_12.csv
│ ├── 15_13.csv
│ ├── 15_14.csv
│ ├── 15_2.csv
│ ├── 15_3.csv
│ ├── 15_4.csv
│ ├── 15_5.csv
│ ├── 15_6.csv
│ ├── 15_7.csv
│ ├── 15_8.csv
│ ├── 15_9.csv
│ ├── 16_1.csv
│ ├── 16_10.csv
│ ├── 16_11.csv
│ ├── 16_12.csv
│ ├── 16_13.csv
│ ├── 16_14.csv
│ ├── 16_2.csv
│ ├── 16_3.csv
│ ├── 16_4.csv
│ ├── 16_5.csv
│ ├── 16_6.csv
│ ├── 16_7.csv
│ ├── 16_8.csv
│ ├── 16_9.csv
│ ├── 17_1.csv
│ ├── 17_10.csv
│ ├── 17_11.csv
│ ├── 17_12.csv
│ ├── 17_13.csv
│ ├── 17_14.csv
│ ├── 17_2.csv
│ ├── 17_3.csv
│ ├── 17_4.csv
│ ├── 17_5.csv
│ ├── 17_6.csv
│ ├── 17_7.csv
│ ├── 17_8.csv
│ ├── 17_9.csv
│ ├── 18_1.csv
│ ├── 18_10.csv
│ ├── 18_11.csv
│ ├── 18_12.csv
│ ├── 18_13.csv
│ ├── 18_14.csv
│ ├── 18_2.csv
│ ├── 18_3.csv
│ ├── 18_4.csv
│ ├── 18_5.csv
│ ├── 18_6.csv
│ ├── 18_7.csv
│ ├── 18_8.csv
│ ├── 18_9.csv
│ ├── 19_1.csv
│ ├── 19_10.csv
│ ├── 19_11.csv
│ ├── 19_12.csv
│ ├── 19_13.csv
│ ├── 19_14.csv
│ ├── 19_2.csv
│ ├── 19_3.csv
│ ├── 19_4.csv
│ ├── 19_5.csv
│ ├── 19_6.csv
│ ├── 19_7.csv
│ ├── 19_8.csv
│ ├── 19_9.csv
│ ├── 20_1.csv
│ ├── 20_10.csv
│ ├── 20_11.csv
│ ├── 20_12.csv
│ ├── 20_13.csv
│ ├── 20_14.csv
│ ├── 20_2.csv
│ ├── 20_3.csv
│ ├── 20_4.csv
│ ├── 20_5.csv
│ ├── 20_6.csv
│ ├── 20_7.csv
│ ├── 20_8.csv
│ ├── 20_9.csv
│ ├── 21_1.csv
│ ├── 21_10.csv
│ ├── 21_11.csv
│ ├── 21_12.csv
│ ├── 21_13.csv
│ ├── 21_14.csv
│ ├── 21_2.csv
│ ├── 21_3.csv
│ ├── 21_4.csv
│ ├── 21_5.csv
│ ├── 21_6.csv
│ ├── 21_7.csv
│ ├── 21_8.csv
│ ├── 21_9.csv
│ ├── 22_1.csv
│ ├── 22_10.csv
│ ├── 22_11.csv
│ ├── 22_12.csv
│ ├── 22_13.csv
│ ├── 22_14.csv
│ ├── 22_2.csv
│ ├── 22_3.csv
│ ├── 22_4.csv
│ ├── 22_5.csv
│ ├── 22_6.csv
│ ├── 22_7.csv
│ ├── 22_8.csv
│ ├── 22_9.csv
│ ├── 23_1.csv
│ ├── 23_10.csv
│ ├── 23_11.csv
│ ├── 23_12.csv
│ ├── 23_13.csv
│ ├── 23_14.csv
│ ├── 23_2.csv
│ ├── 23_3.csv
│ ├── 23_4.csv
│ ├── 23_5.csv
│ ├── 23_6.csv
│ ├── 23_7.csv
│ ├── 23_8.csv
│ ├── 23_9.csv
│ ├── 24_1.csv
│ ├── 24_10.csv
│ ├── 24_11.csv
│ ├── 24_12.csv
│ ├── 24_13.csv
│ ├── 24_14.csv
│ ├── 24_2.csv
│ ├── 24_3.csv
│ ├── 24_4.csv
│ ├── 24_5.csv
│ ├── 24_6.csv
│ ├── 24_7.csv
│ ├── 24_8.csv
│ ├── 24_9.csv
│ ├── 25_1.csv
│ ├── 25_10.csv
│ ├── 25_11.csv
│ ├── 25_12.csv
│ ├── 25_13.csv
│ ├── 25_14.csv
│ ├── 25_2.csv
│ ├── 25_3.csv
│ ├── 25_4.csv
│ ├── 25_5.csv
│ ├── 25_6.csv
│ ├── 25_7.csv
│ ├── 25_8.csv
│ ├── 25_9.csv
│ ├── 26_1.csv
│ ├── 26_10.csv
│ ├── 26_11.csv
│ ├── 26_12.csv
│ ├── 26_13.csv
│ ├── 26_14.csv
│ ├── 26_2.csv
│ ├── 26_3.csv
│ ├── 26_4.csv
│ ├── 26_5.csv
│ ├── 26_6.csv
│ ├── 26_7.csv
│ ├── 26_8.csv
│ ├── 26_9.csv
│ ├── 27_1.csv
│ ├── 27_10.csv
│ ├── 27_11.csv
│ ├── 27_12.csv
│ ├── 27_13.csv
│ ├── 27_14.csv
│ ├── 27_2.csv
│ ├── 27_3.csv
│ ├── 27_4.csv
│ ├── 27_5.csv
│ ├── 27_6.csv
│ ├── 27_7.csv
│ ├── 27_8.csv
│ ├── 27_9.csv
│ ├── 28_1.csv
│ ├── 28_10.csv
│ ├── 28_11.csv
│ ├── 28_12.csv
│ ├── 28_13.csv
│ ├── 28_14.csv
│ ├── 28_2.csv
│ ├── 28_3.csv
│ ├── 28_4.csv
│ ├── 28_5.csv
│ ├── 28_6.csv
│ ├── 28_7.csv
│ ├── 28_8.csv
│ ├── 28_9.csv
│ ├── 29_1.csv
│ ├── 29_10.csv
│ ├── 29_11.csv
│ ├── 29_12.csv
│ ├── 29_13.csv
│ ├── 29_14.csv
│ ├── 29_2.csv
│ ├── 29_3.csv
│ ├── 29_4.csv
│ ├── 29_5.csv
│ ├── 29_6.csv
│ ├── 29_7.csv
│ ├── 29_8.csv
│ ├── 29_9.csv
│ ├── 30_1.csv
│ ├── 30_10.csv
│ ├── 30_11.csv
│ ├── 30_12.csv
│ ├── 30_13.csv
│ ├── 30_14.csv
│ ├── 30_2.csv
│ ├── 30_3.csv
│ ├── 30_4.csv
│ ├── 30_5.csv
│ ├── 30_6.csv
│ ├── 30_7.csv
│ ├── 30_8.csv
│ ├── 30_9.csv
│ ├── 31_1.csv
│ ├── 31_10.csv
│ ├── 31_11.csv
│ ├── 31_12.csv
│ ├── 31_13.csv
│ ├── 31_14.csv
│ ├── 31_2.csv
│ ├── 31_3.csv
│ ├── 31_4.csv
│ ├── 31_5.csv
│ ├── 31_6.csv
│ ├── 31_7.csv
│ ├── 31_8.csv
│ ├── 31_9.csv
│ ├── 32_1.csv
│ ├── 32_10.csv
│ ├── 32_11.csv
│ ├── 32_12.csv
│ ├── 32_13.csv
│ ├── 32_14.csv
│ ├── 32_2.csv
│ ├── 32_3.csv
│ ├── 32_4.csv
│ ├── 32_5.csv
│ ├── 32_6.csv
│ ├── 32_7.csv
│ ├── 32_8.csv
│ ├── 32_9.csv
│ ├── 33_1.csv
│ ├── 33_10.csv
│ ├── 33_11.csv
│ ├── 33_12.csv
│ ├── 33_13.csv
│ ├── 33_14.csv
│ ├── 33_2.csv
│ ├── 33_3.csv
│ ├── 33_4.csv
│ ├── 33_5.csv
│ ├── 33_6.csv
│ ├── 33_7.csv
│ ├── 33_8.csv
│ ├── 33_9.csv
│ ├── 34_1.csv
│ ├── 34_10.csv
│ ├── 34_11.csv
│ ├── 34_12.csv
│ ├── 34_13.csv
│ ├── 34_14.csv
│ ├── 34_2.csv
│ ├── 34_3.csv
│ ├── 34_4.csv
│ ├── 34_5.csv
│ ├── 34_6.csv
│ ├── 34_7.csv
│ ├── 34_8.csv
│ ├── 34_9.csv
│ ├── 35_1.csv
│ ├── 35_10.csv
│ ├── 35_11.csv
│ ├── 35_12.csv
│ ├── 35_13.csv
│ ├── 35_14.csv
│ ├── 35_2.csv
│ ├── 35_3.csv
│ ├── 35_4.csv
│ ├── 35_5.csv
│ ├── 35_6.csv
│ ├── 35_7.csv
│ ├── 35_8.csv
│ ├── 35_9.csv
│ ├── 36_1.csv
│ ├── 36_10.csv
│ ├── 36_11.csv
│ ├── 36_12.csv
│ ├── 36_13.csv
│ ├── 36_14.csv
│ ├── 36_2.csv
│ ├── 36_3.csv
│ ├── 36_4.csv
│ ├── 36_5.csv
│ ├── 36_6.csv
│ ├── 36_7.csv
│ ├── 36_8.csv
│ ├── 36_9.csv
│ ├── 37_1.csv
│ ├── 37_10.csv
│ ├── 37_11.csv
│ ├── 37_12.csv
│ ├── 37_13.csv
│ ├── 37_14.csv
│ ├── 37_2.csv
│ ├── 37_3.csv
│ ├── 37_4.csv
│ ├── 37_5.csv
│ ├── 37_6.csv
│ ├── 37_7.csv
│ ├── 37_8.csv
│ ├── 37_9.csv
│ ├── 38_1.csv
│ ├── 38_10.csv
│ ├── 38_11.csv
│ ├── 38_12.csv
│ ├── 38_13.csv
│ ├── 38_14.csv
│ ├── 38_2.csv
│ ├── 38_3.csv
│ ├── 38_4.csv
│ ├── 38_5.csv
│ ├── 38_6.csv
│ ├── 38_7.csv
│ ├── 38_8.csv
│ ├── 38_9.csv
│ ├── 39_1.csv
│ ├── 39_10.csv
│ ├── 39_11.csv
│ ├── 39_12.csv
│ ├── 39_13.csv
│ ├── 39_14.csv
│ ├── 39_2.csv
│ ├── 39_3.csv
│ ├── 39_4.csv
│ ├── 39_5.csv
│ ├── 39_6.csv
│ ├── 39_7.csv
│ ├── 39_8.csv
│ ├── 39_9.csv
│ ├── 40_1.csv
│ ├── 40_10.csv
│ ├── 40_11.csv
│ ├── 40_12.csv
│ ├── 40_13.csv
│ ├── 40_14.csv
│ ├── 40_2.csv
│ ├── 40_3.csv
│ ├── 40_4.csv
│ ├── 40_5.csv
│ ├── 40_6.csv
│ ├── 40_7.csv
│ ├── 40_8.csv
│ ├── 40_9.csv
│ ├── 41_1.csv
│ ├── 41_10.csv
│ ├── 41_11.csv
│ ├── 41_12.csv
│ ├── 41_13.csv
│ ├── 41_14.csv
│ ├── 41_2.csv
│ ├── 41_3.csv
│ ├── 41_4.csv
│ ├── 41_5.csv
│ ├── 41_6.csv
│ ├── 41_7.csv
│ ├── 41_8.csv
│ ├── 41_9.csv
│ ├── 42_1.csv
│ ├── 42_10.csv
│ ├── 42_11.csv
│ ├── 42_12.csv
│ ├── 42_13.csv
│ ├── 42_14.csv
│ ├── 42_2.csv
│ ├── 42_3.csv
│ ├── 42_4.csv
│ ├── 42_5.csv
│ ├── 42_6.csv
│ ├── 42_7.csv
│ ├── 42_8.csv
│ ├── 42_9.csv
│ ├── 43_1.csv
│ ├── 43_10.csv
│ ├── 43_11.csv
│ ├── 43_12.csv
│ ├── 43_13.csv
│ ├── 43_14.csv
│ ├── 43_2.csv
│ ├── 43_3.csv
│ ├── 43_4.csv
│ ├── 43_5.csv
│ ├── 43_6.csv
│ ├── 43_7.csv
│ ├── 43_8.csv
│ ├── 43_9.csv
│ ├── 44_1.csv
│ ├── 44_10.csv
│ ├── 44_11.csv
│ ├── 44_12.csv
│ ├── 44_13.csv
│ ├── 44_14.csv
│ ├── 44_2.csv
│ ├── 44_3.csv
│ ├── 44_4.csv
│ ├── 44_5.csv
│ ├── 44_6.csv
│ ├── 44_7.csv
│ ├── 44_8.csv
│ ├── 44_9.csv
│ ├── 45_1.csv
│ ├── 45_10.csv
│ ├── 45_11.csv
│ ├── 45_12.csv
│ ├── 45_13.csv
│ ├── 45_14.csv
│ ├── 45_2.csv
│ ├── 45_3.csv
│ ├── 45_4.csv
│ ├── 45_5.csv
│ ├── 45_6.csv
│ ├── 45_7.csv
│ ├── 45_8.csv
│ ├── 45_9.csv
│ ├── 46_1.csv
│ ├── 46_10.csv
│ ├── 46_11.csv
│ ├── 46_12.csv
│ ├── 46_13.csv
│ ├── 46_14.csv
│ ├── 46_2.csv
│ ├── 46_3.csv
│ ├── 46_4.csv
│ ├── 46_5.csv
│ ├── 46_6.csv
│ ├── 46_7.csv
│ ├── 46_8.csv
│ ├── 46_9.csv
│ ├── 47_1.csv
│ ├── 47_10.csv
│ ├── 47_11.csv
│ ├── 47_12.csv
│ ├── 47_13.csv
│ ├── 47_14.csv
│ ├── 47_2.csv
│ ├── 47_3.csv
│ ├── 47_4.csv
│ ├── 47_5.csv
│ ├── 47_6.csv
│ ├── 47_7.csv
│ ├── 47_8.csv
│ ├── 47_9.csv
│ ├── 48_1.csv
│ ├── 48_10.csv
│ ├── 48_11.csv
│ ├── 48_12.csv
│ ├── 48_13.csv
│ ├── 48_14.csv
│ ├── 48_2.csv
│ ├── 48_3.csv
│ ├── 48_4.csv
│ ├── 48_5.csv
│ ├── 48_6.csv
│ ├── 48_7.csv
│ ├── 48_8.csv
│ ├── 48_9.csv
│ ├── 49_1.csv
│ ├── 49_10.csv
│ ├── 49_11.csv
│ ├── 49_12.csv
│ ├── 49_13.csv
│ ├── 49_14.csv
│ ├── 49_2.csv
│ ├── 49_3.csv
│ ├── 49_4.csv
│ ├── 49_5.csv
│ ├── 49_6.csv
│ ├── 49_7.csv
│ ├── 49_8.csv
│ ├── 49_9.csv
│ ├── 50_1.csv
│ ├── 50_10.csv
│ ├── 50_11.csv
│ ├── 50_12.csv
│ ├── 50_13.csv
│ ├── 50_14.csv
│ ├── 50_2.csv
│ ├── 50_3.csv
│ ├── 50_4.csv
│ ├── 50_5.csv
│ ├── 50_6.csv
│ ├── 50_7.csv
│ ├── 50_8.csv
│ ├── 50_9.csv
│ ├── 51_1.csv
│ ├── 51_10.csv
│ ├── 51_11.csv
│ ├── 51_12.csv
│ ├── 51_13.csv
│ ├── 51_14.csv
│ ├── 51_2.csv
│ ├── 51_3.csv
│ ├── 51_4.csv
│ ├── 51_5.csv
│ ├── 51_6.csv
│ ├── 51_7.csv
│ ├── 51_8.csv
│ ├── 51_9.csv
│ ├── 52_1.csv
│ ├── 52_10.csv
│ ├── 52_11.csv
│ ├── 52_12.csv
│ ├── 52_13.csv
│ ├── 52_14.csv
│ ├── 52_2.csv
│ ├── 52_3.csv
│ ├── 52_4.csv
│ ├── 52_5.csv
│ ├── 52_6.csv
│ ├── 52_7.csv
│ ├── 52_8.csv
│ ├── 52_9.csv
│ ├── 53_1.csv
│ ├── 53_10.csv
│ ├── 53_11.csv
│ ├── 53_12.csv
│ ├── 53_13.csv
│ ├── 53_14.csv
│ ├── 53_2.csv
│ ├── 53_3.csv
│ ├── 53_4.csv
│ ├── 53_5.csv
│ ├── 53_6.csv
│ ├── 53_7.csv
│ ├── 53_8.csv
│ ├── 53_9.csv
│ ├── 54_1.csv
│ ├── 54_10.csv
│ ├── 54_11.csv
│ ├── 54_12.csv
│ ├── 54_13.csv
│ ├── 54_14.csv
│ ├── 54_2.csv
│ ├── 54_3.csv
│ ├── 54_4.csv
│ ├── 54_5.csv
│ ├── 54_6.csv
│ ├── 54_7.csv
│ ├── 54_8.csv
│ ├── 54_9.csv
│ ├── 55_1.csv
│ ├── 55_10.csv
│ ├── 55_11.csv
│ ├── 55_12.csv
│ ├── 55_13.csv
│ ├── 55_14.csv
│ ├── 55_2.csv
│ ├── 55_3.csv
│ ├── 55_4.csv
│ ├── 55_5.csv
│ ├── 55_6.csv
│ ├── 55_7.csv
│ ├── 55_8.csv
│ ├── 55_9.csv
│ ├── 56_1.csv
│ ├── 56_10.csv
│ ├── 56_11.csv
│ ├── 56_12.csv
│ ├── 56_13.csv
│ ├── 56_14.csv
│ ├── 56_2.csv
│ ├── 56_3.csv
│ ├── 56_4.csv
│ ├── 56_5.csv
│ ├── 56_6.csv
│ ├── 56_7.csv
│ ├── 56_8.csv
│ ├── 56_9.csv
│ ├── 57_1.csv
│ ├── 57_10.csv
│ ├── 57_11.csv
│ ├── 57_12.csv
│ ├── 57_13.csv
│ ├── 57_14.csv
│ ├── 57_2.csv
│ ├── 57_3.csv
│ ├── 57_4.csv
│ ├── 57_5.csv
│ ├── 57_6.csv
│ ├── 57_7.csv
│ ├── 57_8.csv
│ ├── 57_9.csv
│ ├── 58_1.csv
│ ├── 58_10.csv
│ ├── 58_11.csv
│ ├── 58_12.csv
│ ├── 58_13.csv
│ ├── 58_14.csv
│ ├── 58_2.csv
│ ├── 58_3.csv
│ ├── 58_4.csv
│ ├── 58_5.csv
│ ├── 58_6.csv
│ ├── 58_7.csv
│ ├── 58_8.csv
│ ├── 58_9.csv
│ ├── 59_1.csv
│ ├── 59_10.csv
│ ├── 59_11.csv
│ ├── 59_12.csv
│ ├── 59_13.csv
│ ├── 59_14.csv
│ ├── 59_2.csv
│ ├── 59_3.csv
│ ├── 59_4.csv
│ ├── 59_5.csv
│ ├── 59_6.csv
│ ├── 59_7.csv
│ ├── 59_8.csv
│ ├── 59_9.csv
│ ├── 60_1.csv
│ ├── 60_10.csv
│ ├── 60_11.csv
│ ├── 60_12.csv
│ ├── 60_13.csv
│ ├── 60_14.csv
│ ├── 60_2.csv
│ ├── 60_3.csv
│ ├── 60_4.csv
│ ├── 60_5.csv
│ ├── 60_6.csv
│ ├── 60_7.csv
│ ├── 60_8.csv
│ ├── 60_9.csv
│ ├── 61_1.csv
│ ├── 61_10.csv
│ ├── 61_11.csv
│ ├── 61_12.csv
│ ├── 61_13.csv
│ ├── 61_14.csv
│ ├── 61_2.csv
│ ├── 61_3.csv
│ ├── 61_4.csv
│ ├── 61_5.csv
│ ├── 61_6.csv
│ ├── 61_7.csv
│ ├── 61_8.csv
│ ├── 61_9.csv
│ ├── 62_1.csv
│ ├── 62_10.csv
│ ├── 62_11.csv
│ ├── 62_12.csv
│ ├── 62_13.csv
│ ├── 62_14.csv
│ ├── 62_2.csv
│ ├── 62_3.csv
│ ├── 62_4.csv
│ ├── 62_5.csv
│ ├── 62_6.csv
│ ├── 62_7.csv
│ ├── 62_8.csv
│ ├── 62_9.csv
│ ├── 63_1.csv
│ ├── 63_10.csv
│ ├── 63_11.csv
│ ├── 63_12.csv
│ ├── 63_13.csv
│ ├── 63_14.csv
│ ├── 63_2.csv
│ ├── 63_3.csv
│ ├── 63_4.csv
│ ├── 63_5.csv
│ ├── 63_6.csv
│ ├── 63_7.csv
│ ├── 63_8.csv
│ ├── 63_9.csv
│ ├── 64_1.csv
│ ├── 64_10.csv
│ ├── 64_11.csv
│ ├── 64_12.csv
│ ├── 64_13.csv
│ ├── 64_14.csv
│ ├── 64_2.csv
│ ├── 64_3.csv
│ ├── 64_4.csv
│ ├── 64_5.csv
│ ├── 64_6.csv
│ ├── 64_7.csv
│ ├── 64_8.csv
│ ├── 64_9.csv
│ ├── 65_1.csv
│ ├── 65_10.csv
│ ├── 65_11.csv
│ ├── 65_12.csv
│ ├── 65_13.csv
│ ├── 65_14.csv
│ ├── 65_2.csv
│ ├── 65_3.csv
│ ├── 65_4.csv
│ ├── 65_5.csv
│ ├── 65_6.csv
│ ├── 65_7.csv
│ ├── 65_8.csv
│ ├── 65_9.csv
│ ├── 66_1.csv
│ ├── 66_10.csv
│ ├── 66_11.csv
│ ├── 66_12.csv
│ ├── 66_13.csv
│ ├── 66_14.csv
│ ├── 66_2.csv
│ ├── 66_3.csv
│ ├── 66_4.csv
│ ├── 66_5.csv
│ ├── 66_6.csv
│ ├── 66_7.csv
│ ├── 66_8.csv
│ ├── 66_9.csv
│ ├── 67_1.csv
│ ├── 67_10.csv
│ ├── 67_11.csv
│ ├── 67_12.csv
│ ├── 67_13.csv
│ ├── 67_14.csv
│ ├── 67_2.csv
│ ├── 67_3.csv
│ ├── 67_4.csv
│ ├── 67_5.csv
│ ├── 67_6.csv
│ ├── 67_7.csv
│ ├── 67_8.csv
│ ├── 67_9.csv
│ ├── 68_1.csv
│ ├── 68_10.csv
│ ├── 68_11.csv
│ ├── 68_12.csv
│ ├── 68_13.csv
│ ├── 68_14.csv
│ ├── 68_2.csv
│ ├── 68_3.csv
│ ├── 68_4.csv
│ ├── 68_5.csv
│ ├── 68_6.csv
│ ├── 68_7.csv
│ ├── 68_8.csv
│ ├── 68_9.csv
│ ├── 69_1.csv
│ ├── 69_10.csv
│ ├── 69_11.csv
│ ├── 69_12.csv
│ ├── 69_13.csv
│ ├── 69_14.csv
│ ├── 69_2.csv
│ ├── 69_3.csv
│ ├── 69_4.csv
│ ├── 69_5.csv
│ ├── 69_6.csv
│ ├── 69_7.csv
│ ├── 69_8.csv
│ ├── 69_9.csv
│ ├── 6_1.csv
│ ├── 6_10.csv
│ ├── 6_11.csv
│ ├── 6_12.csv
│ ├── 6_13.csv
│ ├── 6_14.csv
│ ├── 6_2.csv
│ ├── 6_3.csv
│ ├── 6_4.csv
│ ├── 6_5.csv
│ ├── 6_6.csv
│ ├── 6_7.csv
│ ├── 6_8.csv
│ ├── 6_9.csv
│ ├── 70_1.csv
│ ├── 70_10.csv
│ ├── 70_11.csv
│ ├── 70_12.csv
│ ├── 70_13.csv
│ ├── 70_14.csv
│ ├── 70_2.csv
│ ├── 70_3.csv
│ ├── 70_4.csv
│ ├── 70_5.csv
│ ├── 70_6.csv
│ ├── 70_7.csv
│ ├── 70_8.csv
│ ├── 70_9.csv
│ ├── 71_1.csv
│ ├── 71_10.csv
│ ├── 71_11.csv
│ ├── 71_12.csv
│ ├── 71_13.csv
│ ├── 71_14.csv
│ ├── 71_2.csv
│ ├── 71_3.csv
│ ├── 71_4.csv
│ ├── 71_5.csv
│ ├── 71_6.csv
│ ├── 71_7.csv
│ ├── 71_8.csv
│ ├── 71_9.csv
│ ├── 72_1.csv
│ ├── 72_10.csv
│ ├── 72_11.csv
│ ├── 72_12.csv
│ ├── 72_13.csv
│ ├── 72_14.csv
│ ├── 72_2.csv
│ ├── 72_3.csv
│ ├── 72_4.csv
│ ├── 72_5.csv
│ ├── 72_6.csv
│ ├── 72_7.csv
│ ├── 72_8.csv
│ ├── 72_9.csv
│ ├── 73_1.csv
│ ├── 73_10.csv
│ ├── 73_11.csv
│ ├── 73_12.csv
│ ├── 73_13.csv
│ ├── 73_14.csv
│ ├── 73_2.csv
│ ├── 73_3.csv
│ ├── 73_4.csv
│ ├── 73_5.csv
│ ├── 73_6.csv
│ ├── 73_7.csv
│ ├── 73_8.csv
│ ├── 73_9.csv
│ ├── 74_1.csv
│ ├── 74_10.csv
│ ├── 74_11.csv
│ ├── 74_12.csv
│ ├── 74_13.csv
│ ├── 74_14.csv
│ ├── 74_2.csv
│ ├── 74_3.csv
│ ├── 74_4.csv
│ ├── 74_5.csv
│ ├── 74_6.csv
│ ├── 74_7.csv
│ ├── 74_8.csv
│ ├── 74_9.csv
│ ├── 75_1.csv
│ ├── 75_10.csv
│ ├── 75_11.csv
│ ├── 75_12.csv
│ ├── 75_13.csv
│ ├── 75_14.csv
│ ├── 75_2.csv
│ ├── 75_3.csv
│ ├── 75_4.csv
│ ├── 75_5.csv
│ ├── 75_6.csv
│ ├── 75_7.csv
│ ├── 75_8.csv
│ ├── 75_9.csv
│ ├── 76_1.csv
│ ├── 76_10.csv
│ ├── 76_11.csv
│ ├── 76_12.csv
│ ├── 76_13.csv
│ ├── 76_14.csv
│ ├── 76_2.csv
│ ├── 76_3.csv
│ ├── 76_4.csv
│ ├── 76_5.csv
│ ├── 76_6.csv
│ ├── 76_7.csv
│ ├── 76_8.csv
│ ├── 76_9.csv
│ ├── 77_1.csv
│ ├── 77_10.csv
│ ├── 77_11.csv
│ ├── 77_12.csv
│ ├── 77_13.csv
│ ├── 77_14.csv
│ ├── 77_2.csv
│ ├── 77_3.csv
│ ├── 77_4.csv
│ ├── 77_5.csv
│ ├── 77_6.csv
│ ├── 77_7.csv
│ ├── 77_8.csv
│ ├── 77_9.csv
│ ├── 78_1.csv
│ ├── 78_10.csv
│ ├── 78_11.csv
│ ├── 78_12.csv
│ ├── 78_13.csv
│ ├── 78_14.csv
│ ├── 78_2.csv
│ ├── 78_3.csv
│ ├── 78_4.csv
│ ├── 78_5.csv
│ ├── 78_6.csv
│ ├── 78_7.csv
│ ├── 78_8.csv
│ ├── 78_9.csv
│ ├── 79_1.csv
│ ├── 79_10.csv
│ ├── 79_11.csv
│ ├── 79_12.csv
│ ├── 79_13.csv
│ ├── 79_14.csv
│ ├── 79_2.csv
│ ├── 79_3.csv
│ ├── 79_4.csv
│ ├── 79_5.csv
│ ├── 79_6.csv
│ ├── 79_7.csv
│ ├── 79_8.csv
│ ├── 79_9.csv
│ ├── 7_1.csv
│ ├── 7_10.csv
│ ├── 7_11.csv
│ ├── 7_12.csv
│ ├── 7_13.csv
│ ├── 7_14.csv
│ ├── 7_2.csv
│ ├── 7_3.csv
│ ├── 7_4.csv
│ ├── 7_5.csv
│ ├── 7_6.csv
│ ├── 7_7.csv
│ ├── 7_8.csv
│ ├── 7_9.csv
│ ├── 80_1.csv
│ ├── 80_10.csv
│ ├── 80_11.csv
│ ├── 80_12.csv
│ ├── 80_13.csv
│ ├── 80_14.csv
│ ├── 80_2.csv
│ ├── 80_3.csv
│ ├── 80_4.csv
│ ├── 80_5.csv
│ ├── 80_6.csv
│ ├── 80_7.csv
│ ├── 80_8.csv
│ ├── 80_9.csv
│ ├── 81_1.csv
│ ├── 81_10.csv
│ ├── 81_11.csv
│ ├── 81_12.csv
│ ├── 81_13.csv
│ ├── 81_14.csv
│ ├── 81_2.csv
│ ├── 81_3.csv
│ ├── 81_4.csv
│ ├── 81_5.csv
│ ├── 81_6.csv
│ ├── 81_7.csv
│ ├── 81_8.csv
│ ├── 81_9.csv
│ ├── 82_1.csv
│ ├── 82_10.csv
│ ├── 82_11.csv
│ ├── 82_12.csv
│ ├── 82_13.csv
│ ├── 82_14.csv
│ ├── 82_2.csv
│ ├── 82_3.csv
│ ├── 82_4.csv
│ ├── 82_5.csv
│ ├── 82_6.csv
│ ├── 82_7.csv
│ ├── 82_8.csv
│ ├── 82_9.csv
│ ├── 83_1.csv
│ ├── 83_10.csv
│ ├── 83_11.csv
│ ├── 83_12.csv
│ ├── 83_13.csv
│ ├── 83_14.csv
│ ├── 83_2.csv
│ ├── 83_3.csv
│ ├── 83_4.csv
│ ├── 83_5.csv
│ ├── 83_6.csv
│ ├── 83_7.csv
│ ├── 83_8.csv
│ ├── 83_9.csv
│ ├── 84_1.csv
│ ├── 84_10.csv
│ ├── 84_11.csv
│ ├── 84_12.csv
│ ├── 84_13.csv
│ ├── 84_14.csv
│ ├── 84_2.csv
│ ├── 84_3.csv
│ ├── 84_4.csv
│ ├── 84_5.csv
│ ├── 84_6.csv
│ ├── 84_7.csv
│ ├── 84_8.csv
│ ├── 84_9.csv
│ ├── 85_1.csv
│ ├── 85_10.csv
│ ├── 85_11.csv
│ ├── 85_12.csv
│ ├── 85_13.csv
│ ├── 85_14.csv
│ ├── 85_2.csv
│ ├── 85_3.csv
│ ├── 85_4.csv
│ ├── 85_5.csv
│ ├── 85_6.csv
│ ├── 85_7.csv
│ ├── 85_8.csv
│ ├── 85_9.csv
│ ├── 86_1.csv
│ ├── 86_10.csv
│ ├── 86_11.csv
│ ├── 86_12.csv
│ ├── 86_13.csv
│ ├── 86_14.csv
│ ├── 86_2.csv
│ ├── 86_3.csv
│ ├── 86_4.csv
│ ├── 86_5.csv
│ ├── 86_6.csv
│ ├── 86_7.csv
│ ├── 86_8.csv
│ ├── 86_9.csv
│ ├── 87_1.csv
│ ├── 87_10.csv
│ ├── 87_11.csv
│ ├── 87_12.csv
│ ├── 87_13.csv
│ ├── 87_14.csv
│ ├── 87_2.csv
│ ├── 87_3.csv
│ ├── 87_4.csv
│ ├── 87_5.csv
│ ├── 87_6.csv
│ ├── 87_7.csv
│ ├── 87_8.csv
│ ├── 87_9.csv
│ ├── 88_1.csv
│ ├── 88_10.csv
│ ├── 88_11.csv
│ ├── 88_12.csv
│ ├── 88_13.csv
│ ├── 88_14.csv
│ ├── 88_2.csv
│ ├── 88_3.csv
│ ├── 88_4.csv
│ ├── 88_5.csv
│ ├── 88_6.csv
│ ├── 88_7.csv
│ ├── 88_8.csv
│ ├── 88_9.csv
│ ├── 89_1.csv
│ ├── 89_10.csv
│ ├── 89_11.csv
│ ├── 89_12.csv
│ ├── 89_13.csv
│ ├── 89_14.csv
│ ├── 89_2.csv
│ ├── 89_3.csv
│ ├── 89_4.csv
│ ├── 89_5.csv
│ ├── 89_6.csv
│ ├── 89_7.csv
│ ├── 89_8.csv
│ ├── 89_9.csv
│ ├── 8_1.csv
│ ├── 8_10.csv
│ ├── 8_11.csv
│ ├── 8_12.csv
│ ├── 8_13.csv
│ ├── 8_14.csv
│ ├── 8_2.csv
│ ├── 8_3.csv
│ ├── 8_4.csv
│ ├── 8_5.csv
│ ├── 8_6.csv
│ ├── 8_7.csv
│ ├── 8_8.csv
│ ├── 8_9.csv
│ ├── 90_1.csv
│ ├── 90_10.csv
│ ├── 90_11.csv
│ ├── 90_12.csv
│ ├── 90_13.csv
│ ├── 90_14.csv
│ ├── 90_2.csv
│ ├── 90_3.csv
│ ├── 90_4.csv
│ ├── 90_5.csv
│ ├── 90_6.csv
│ ├── 90_7.csv
│ ├── 90_8.csv
│ ├── 90_9.csv
│ ├── 91_1.csv
│ ├── 91_10.csv
│ ├── 91_11.csv
│ ├── 91_12.csv
│ ├── 91_13.csv
│ ├── 91_14.csv
│ ├── 91_2.csv
│ ├── 91_3.csv
│ ├── 91_4.csv
│ ├── 91_5.csv
│ ├── 91_6.csv
│ ├── 91_7.csv
│ ├── 91_8.csv
│ ├── 91_9.csv
│ ├── 92_1.csv
│ ├── 92_10.csv
│ ├── 92_11.csv
│ ├── 92_12.csv
│ ├── 92_13.csv
│ ├── 92_14.csv
│ ├── 92_2.csv
│ ├── 92_3.csv
│ ├── 92_4.csv
│ ├── 92_5.csv
│ ├── 92_6.csv
│ ├── 92_7.csv
│ ├── 92_8.csv
│ ├── 92_9.csv
│ ├── 93_1.csv
│ ├── 93_10.csv
│ ├── 93_11.csv
│ ├── 93_12.csv
│ ├── 93_13.csv
│ ├── 93_14.csv
│ ├── 93_2.csv
│ ├── 93_3.csv
│ ├── 93_4.csv
│ ├── 93_5.csv
│ ├── 93_6.csv
│ ├── 93_7.csv
│ ├── 93_8.csv
│ ├── 93_9.csv
│ ├── 94_1.csv
│ ├── 94_10.csv
│ ├── 94_11.csv
│ ├── 94_12.csv
│ ├── 94_13.csv
│ ├── 94_14.csv
│ ├── 94_2.csv
│ ├── 94_3.csv
│ ├── 94_4.csv
│ ├── 94_5.csv
│ ├── 94_6.csv
│ ├── 94_7.csv
│ ├── 94_8.csv
│ ├── 94_9.csv
│ ├── 95_1.csv
│ ├── 95_10.csv
│ ├── 95_11.csv
│ ├── 95_12.csv
│ ├── 95_13.csv
│ ├── 95_14.csv
│ ├── 95_2.csv
│ ├── 95_3.csv
│ ├── 95_4.csv
│ ├── 95_5.csv
│ ├── 95_6.csv
│ ├── 95_7.csv
│ ├── 95_8.csv
│ ├── 95_9.csv
│ ├── 96_1.csv
│ ├── 96_10.csv
│ ├── 96_11.csv
│ ├── 96_12.csv
│ ├── 96_13.csv
│ ├── 96_14.csv
│ ├── 96_2.csv
│ ├── 96_3.csv
│ ├── 96_4.csv
│ ├── 96_5.csv
│ ├── 96_6.csv
│ ├── 96_7.csv
│ ├── 96_8.csv
│ ├── 96_9.csv
│ ├── 97_1.csv
│ ├── 97_10.csv
│ ├── 97_11.csv
│ ├── 97_12.csv
│ ├── 97_13.csv
│ ├── 97_14.csv
│ ├── 97_2.csv
│ ├── 97_3.csv
│ ├── 97_4.csv
│ ├── 97_5.csv
│ ├── 97_6.csv
│ ├── 97_7.csv
│ ├── 97_8.csv
│ ├── 97_9.csv
│ ├── 98_1.csv
│ ├── 98_10.csv
│ ├── 98_11.csv
│ ├── 98_12.csv
│ ├── 98_13.csv
│ ├── 98_14.csv
│ ├── 98_2.csv
│ ├── 98_3.csv
│ ├── 98_4.csv
│ ├── 98_5.csv
│ ├── 98_6.csv
│ ├── 98_7.csv
│ ├── 98_8.csv
│ ├── 98_9.csv
│ ├── 99_1.csv
│ ├── 99_10.csv
│ ├── 99_11.csv
│ ├── 99_12.csv
│ ├── 99_13.csv
│ ├── 99_14.csv
│ ├── 99_2.csv
│ ├── 99_3.csv
│ ├── 99_4.csv
│ ├── 99_5.csv
│ ├── 99_6.csv
│ ├── 99_7.csv
│ ├── 99_8.csv
│ ├── 99_9.csv
│ ├── 9_1.csv
│ ├── 9_10.csv
│ ├── 9_11.csv
│ ├── 9_12.csv
│ ├── 9_13.csv
│ ├── 9_14.csv
│ ├── 9_2.csv
│ ├── 9_3.csv
│ ├── 9_4.csv
│ ├── 9_5.csv
│ ├── 9_6.csv
│ ├── 9_7.csv
│ ├── 9_8.csv
│ └── 9_9.csv
├── static/
│ ├── css/
│ │ ├── alliance_station_display.css
│ │ ├── audience_display.css
│ │ ├── bracket_display.css
│ │ ├── cheesy-arena.css
│ │ ├── field_monitor_display.css
│ │ ├── fonts/
│ │ │ ├── futura-lt-bold.otf
│ │ │ └── futura-lt.otf
│ │ ├── logo_display.css
│ │ ├── placeholder_display.css
│ │ ├── queueing_display.css
│ │ ├── rankings_display.css
│ │ ├── referee_panel.css
│ │ ├── scoring_panel.css
│ │ ├── twitch_display.css
│ │ ├── wall_display.css
│ │ └── webpage_display.css
│ ├── js/
│ │ ├── alliance_selection.js
│ │ ├── alliance_station_display.js
│ │ ├── announcer_display.js
│ │ ├── audience_display.js
│ │ ├── bracket_display.js
│ │ ├── cheesy-common.js
│ │ ├── cheesy-websocket.js
│ │ ├── field_monitor_display.js
│ │ ├── lib/
│ │ │ ├── handlebars-1.3.0.js
│ │ │ └── jquery.websocket-0.0.1.js
│ │ ├── logo_display.js
│ │ ├── lower_thirds.js
│ │ ├── match_play.js
│ │ ├── match_review.js
│ │ ├── match_timing.js
│ │ ├── placeholder_display.js
│ │ ├── queueing_display.js
│ │ ├── rankings_display.js
│ │ ├── referee_panel.js
│ │ ├── scoring_panel.js
│ │ ├── setup_displays.js
│ │ ├── setup_field_testing.js
│ │ ├── setup_schedule.js
│ │ ├── twitch_display.js
│ │ ├── wall_display.js
│ │ └── webpage_display.js
│ └── manifest/
│ ├── blue_far_scoring.manifest
│ ├── blue_near_scoring.manifest
│ ├── red_far_scoring.manifest
│ ├── red_near_scoring.manifest
│ └── referee.manifest
├── switch_config.txt
├── templates/
│ ├── alliance_selection.html
│ ├── alliance_station_display.html
│ ├── announcer_display.html
│ ├── announcer_display_match_load.html
│ ├── announcer_display_score_posted.html
│ ├── audience_display.html
│ ├── audience_display_radio_buttons.html
│ ├── backups.csv
│ ├── base.html
│ ├── bracket_display.html
│ ├── bracket_report.html
│ ├── edit_match_result.html
│ ├── edit_team.html
│ ├── field_monitor_display.html
│ ├── fta.csv
│ ├── index.html
│ ├── login.html
│ ├── logo_display.html
│ ├── match_logs.html
│ ├── match_play.html
│ ├── match_play_match_load.html
│ ├── match_review.html
│ ├── placeholder_display.html
│ ├── queueing_display.html
│ ├── queueing_display_match_load.html
│ ├── rankings.csv
│ ├── rankings_display.html
│ ├── referee_panel.html
│ ├── referee_panel_foul_list.html
│ ├── schedule.csv
│ ├── scoring_panel.html
│ ├── setup_awards.html
│ ├── setup_breaks.html
│ ├── setup_displays.html
│ ├── setup_field_testing.html
│ ├── setup_judging.html
│ ├── setup_lower_thirds.html
│ ├── setup_schedule.html
│ ├── setup_settings.html
│ ├── setup_sponsor_slides.html
│ ├── setup_teams.html
│ ├── teams.csv
│ ├── twitch_display.html
│ ├── view_match_log.html
│ ├── wall_display.html
│ └── webpage_display.html
├── tournament/
│ ├── awards.go
│ ├── awards_test.go
│ ├── judging_schedule.go
│ ├── judging_schedule_test.go
│ ├── qualification_rankings.go
│ ├── qualification_rankings_test.go
│ ├── schedule.go
│ ├── schedule_test.go
│ └── test_helpers.go
├── tunnel
├── tunnel_nginx_config
├── web/
│ ├── alliance_selection.go
│ ├── alliance_selection_test.go
│ ├── alliance_station_display.go
│ ├── alliance_station_display_test.go
│ ├── announcer_display.go
│ ├── announcer_display_test.go
│ ├── api.go
│ ├── api_test.go
│ ├── audience_display.go
│ ├── audience_display_test.go
│ ├── bracket_display.go
│ ├── bracket_display_test.go
│ ├── display_utils.go
│ ├── field_monitor_display.go
│ ├── field_monitor_display_test.go
│ ├── login.go
│ ├── login_test.go
│ ├── logo_display.go
│ ├── logo_display_test.go
│ ├── match_logs.go
│ ├── match_play.go
│ ├── match_play_test.go
│ ├── match_review.go
│ ├── match_review_test.go
│ ├── placeholder_display.go
│ ├── placeholder_display_test.go
│ ├── queueing_display.go
│ ├── queueing_display_test.go
│ ├── rankings_display.go
│ ├── rankings_display_test.go
│ ├── referee_panel.go
│ ├── referee_panel_test.go
│ ├── reports.go
│ ├── reports_test.go
│ ├── scoring_panel.go
│ ├── scoring_panel_test.go
│ ├── setup_awards.go
│ ├── setup_awards_test.go
│ ├── setup_breaks.go
│ ├── setup_breaks_test.go
│ ├── setup_displays.go
│ ├── setup_displays_test.go
│ ├── setup_field_testing.go
│ ├── setup_field_testing_test.go
│ ├── setup_judging.go
│ ├── setup_judging_test.go
│ ├── setup_lower_thirds.go
│ ├── setup_lower_thirds_test.go
│ ├── setup_schedule.go
│ ├── setup_schedule_test.go
│ ├── setup_settings.go
│ ├── setup_settings_test.go
│ ├── setup_sponsor_slides.go
│ ├── setup_sponsor_slides_test.go
│ ├── setup_teams.go
│ ├── setup_teams_test.go
│ ├── twitch_display.go
│ ├── twitch_display_test.go
│ ├── wall_display.go
│ ├── wall_display_test.go
│ ├── web.go
│ ├── web_test.go
│ ├── webpage_display.go
│ └── webpage_display_test.go
└── websocket/
├── notifier.go
├── notifier_test.go
├── websocket.go
└── websocket_test.go
SYMBOL INDEX (1220 symbols across 177 files)
FILE: field/arena.go
constant arenaLoopPeriodMs (line 26) | arenaLoopPeriodMs = 10
constant arenaLoopWarningMs (line 27) | arenaLoopWarningMs = 5
constant dsPacketPeriodMs (line 28) | dsPacketPeriodMs = 500
constant dsPacketWarningMs (line 29) | dsPacketWarningMs = 550
constant periodicTaskPeriodSec (line 30) | periodicTaskPeriodSec = 30
constant matchEndScoreDwellSec (line 31) | matchEndScoreDwellSec = 3
constant postTimeoutSec (line 32) | postTimeoutSec = 4
constant preLoadNextMatchDelaySec (line 33) | preLoadNextMatchDelaySec = 5
constant scheduledBreakDelaySec (line 34) | scheduledBreakDelaySec = 5
constant earlyLateThresholdMin (line 35) | earlyLateThresholdMin = 2.5
constant MaxMatchGapMin (line 36) | MaxMatchGapMin = 20
type MatchState (line 40) | type MatchState
constant PreMatch (line 43) | PreMatch MatchState = iota
constant StartMatch (line 44) | StartMatch
constant WarmupPeriod (line 45) | WarmupPeriod
constant AutoPeriod (line 46) | AutoPeriod
constant PausePeriod (line 47) | PausePeriod
constant TeleopPeriod (line 48) | TeleopPeriod
constant PostMatch (line 49) | PostMatch
constant TimeoutActive (line 50) | TimeoutActive
constant PostTimeout (line 51) | PostTimeout
type Arena (line 54) | type Arena struct
method LoadSettings (line 161) | func (arena *Arena) LoadSettings() error {
method CreatePlayoffTournament (line 294) | func (arena *Arena) CreatePlayoffTournament() error {
method CreatePlayoffMatches (line 303) | func (arena *Arena) CreatePlayoffMatches(startTime time.Time) error {
method UpdatePlayoffTournament (line 308) | func (arena *Arena) UpdatePlayoffTournament() error {
method LoadMatch (line 320) | func (arena *Arena) LoadMatch(match *model.Match) error {
method LoadTestMatch (line 404) | func (arena *Arena) LoadTestMatch() error {
method LoadNextMatch (line 409) | func (arena *Arena) LoadNextMatch(startScheduledBreak bool) error {
method SubstituteTeams (line 440) | func (arena *Arena) SubstituteTeams(red1, red2, red3, blue1, blue2, bl...
method StartMatch (line 493) | func (arena *Arena) StartMatch() error {
method AbortMatch (line 533) | func (arena *Arena) AbortMatch() error {
method ResetMatch (line 557) | func (arena *Arena) ResetMatch() error {
method StartTimeout (line 576) | func (arena *Arena) StartTimeout(description string, durationSec int) ...
method SetAudienceDisplayMode (line 597) | func (arena *Arena) SetAudienceDisplayMode(mode string) {
method SetAllianceStationDisplayMode (line 615) | func (arena *Arena) SetAllianceStationDisplayMode(mode string) {
method MatchTimeSec (line 623) | func (arena *Arena) MatchTimeSec() float64 {
method Update (line 633) | func (arena *Arena) Update() {
method checkEndgameStart (line 774) | func (arena *Arena) checkEndgameStart(matchTimeSec float64) {
method Run (line 793) | func (arena *Arena) Run() {
method RedScoreSummary (line 816) | func (arena *Arena) RedScoreSummary() *game.ScoreSummary {
method BlueScoreSummary (line 821) | func (arena *Arena) BlueScoreSummary() *game.ScoreSummary {
method validateTeams (line 826) | func (arena *Arena) validateTeams(teamIds ...int) error {
method assignTeam (line 843) | func (arena *Arena) assignTeam(teamId int, station string) error {
method getNextMatch (line 883) | func (arena *Arena) getNextMatch(excludeCurrent bool) (*model.Match, e...
method preLoadNextMatch (line 903) | func (arena *Arena) preLoadNextMatch() {
method setSCCEthernetEnabled (line 942) | func (arena *Arena) setSCCEthernetEnabled(enabled bool) {
method setupNetwork (line 961) | func (arena *Arena) setupNetwork(teams [6]*model.Team, isPreload bool) {
method checkCanStartMatch (line 988) | func (arena *Arena) checkCanStartMatch() error {
method checkAllianceStationsReady (line 1015) | func (arena *Arena) checkAllianceStationsReady(stations ...string) err...
method sendDsPacket (line 1034) | func (arena *Arena) sendDsPacket(auto bool, enabled bool) {
method getAssignedAllianceStation (line 1054) | func (arena *Arena) getAssignedAllianceStation(teamId int) string {
method handlePlcInputOutput (line 1065) | func (arena *Arena) handlePlcInputOutput() {
method handleTeamStop (line 1186) | func (arena *Arena) handleTeamStop(station string, eStopState, aStopSt...
method handleSounds (line 1203) | func (arena *Arena) handleSounds(matchTimeSec float64) {
method PlaySound (line 1223) | func (arena *Arena) PlaySound(name string) {
method positionPostMatchScoreReady (line 1229) | func (arena *Arena) positionPostMatchScoreReady(position string) bool {
method runPeriodicTasks (line 1235) | func (arena *Arena) runPeriodicTasks() {
type AllianceStation (line 103) | type AllianceStation struct
function NewArena (line 116) | func NewArena(dbPath string) (*Arena, error) {
function trussLightWarningSequence (line 1242) | func trussLightWarningSequence(matchTimeSec float64) (bool, [3]bool) {
FILE: field/arena_notifiers.go
type ArenaNotifiers (line 16) | type ArenaNotifiers struct
type MatchTimeMessage (line 34) | type MatchTimeMessage struct
type audienceAllianceScoreFields (line 39) | type audienceAllianceScoreFields struct
method configureNotifiers (line 45) | func (arena *Arena) configureNotifiers() {
method generateAllianceSelectionMessage (line 69) | func (arena *Arena) generateAllianceSelectionMessage() any {
method generateAllianceStationDisplayModeMessage (line 83) | func (arena *Arena) generateAllianceStationDisplayModeMessage() any {
method generateArenaStatusMessage (line 87) | func (arena *Arena) generateArenaStatusMessage() any {
method generateAudienceDisplayModeMessage (line 115) | func (arena *Arena) generateAudienceDisplayModeMessage() any {
method generateDisplayConfigurationMessage (line 119) | func (arena *Arena) generateDisplayConfigurationMessage() any {
method generateEventStatusMessage (line 130) | func (arena *Arena) generateEventStatusMessage() any {
method generateLowerThirdMessage (line 134) | func (arena *Arena) generateLowerThirdMessage() any {
method GenerateMatchLoadMessage (line 141) | func (arena *Arena) GenerateMatchLoadMessage() any {
method generateMatchTimeMessage (line 204) | func (arena *Arena) generateMatchTimeMessage() any {
method generateMatchTimingMessage (line 208) | func (arena *Arena) generateMatchTimingMessage() any {
method generateRealtimeScoreMessage (line 212) | func (arena *Arena) generateRealtimeScoreMessage() any {
method GenerateScorePostedMessage (line 229) | func (arena *Arena) GenerateScorePostedMessage() any {
method generateScoringStatusMessage (line 322) | func (arena *Arena) generateScoringStatusMessage() any {
function getAudienceAllianceScoreFields (line 351) | func getAudienceAllianceScoreFields(
function getRulesViolated (line 362) | func getRulesViolated(redFouls, blueFouls []game.Foul) map[int]*game.Rule {
FILE: field/arena_test.go
function TestAssignTeam (line 22) | func TestAssignTeam(t *testing.T) {
function TestArenaCheckCanStartMatch (line 63) | func TestArenaCheckCanStartMatch(t *testing.T) {
function TestArenaMatchFlow (line 93) | func TestArenaMatchFlow(t *testing.T) {
function TestArenaStateEnforcement (line 227) | func TestArenaStateEnforcement(t *testing.T) {
function TestMatchStartRobotLinkEnforcement (line 319) | func TestMatchStartRobotLinkEnforcement(t *testing.T) {
function TestLoadNextMatch (line 405) | func TestLoadNextMatch(t *testing.T) {
function TestSubstituteTeam (line 456) | func TestSubstituteTeam(t *testing.T) {
function TestLoadTeamsFromNexus (line 512) | func TestLoadTeamsFromNexus(t *testing.T) {
function TestArenaTimeout (line 586) | func TestArenaTimeout(t *testing.T) {
function TestSaveTeamHasConnected (line 652) | func TestSaveTeamHasConnected(t *testing.T) {
function TestPlcEStopAStop (line 689) | func TestPlcEStopAStop(t *testing.T) {
function TestPlcEStopAStopWithPlcDisabled (line 870) | func TestPlcEStopAStopWithPlcDisabled(t *testing.T) {
function TestPlcFieldEStop (line 906) | func TestPlcFieldEStop(t *testing.T) {
function TestPlcFieldEStopWithPlcDisabled (line 930) | func TestPlcFieldEStopWithPlcDisabled(t *testing.T) {
function TestPlcMatchCycleEvergreen (line 954) | func TestPlcMatchCycleEvergreen(t *testing.T) {
function TestPlcMatchCycleGameSpecificWithCoopEnabled (line 1037) | func TestPlcMatchCycleGameSpecificWithCoopEnabled(t *testing.T) {
function TestPlcMatchCycleGameSpecificWithCoopDisabled (line 1189) | func TestPlcMatchCycleGameSpecificWithCoopDisabled(t *testing.T) {
FILE: field/display.go
constant minDisplayId (line 21) | minDisplayId = 100
constant displayPurgeTtlMin (line 22) | displayPurgeTtlMin = 30
type DisplayType (line 25) | type DisplayType
constant InvalidDisplay (line 28) | InvalidDisplay DisplayType = iota
constant PlaceholderDisplay (line 29) | PlaceholderDisplay
constant AllianceStationDisplay (line 30) | AllianceStationDisplay
constant AnnouncerDisplay (line 31) | AnnouncerDisplay
constant AudienceDisplay (line 32) | AudienceDisplay
constant BracketDisplay (line 33) | BracketDisplay
constant FieldMonitorDisplay (line 34) | FieldMonitorDisplay
constant LogoDisplay (line 35) | LogoDisplay
constant QueueingDisplay (line 36) | QueueingDisplay
constant RankingsDisplay (line 37) | RankingsDisplay
constant TwitchStreamDisplay (line 38) | TwitchStreamDisplay
constant WallDisplay (line 39) | WallDisplay
constant WebpageDisplay (line 40) | WebpageDisplay
type Display (line 75) | type Display struct
method ToUrl (line 126) | func (display *Display) ToUrl() string {
method generateDisplayConfigurationMessage (line 151) | func (display *Display) generateDisplayConfigurationMessage() any {
type DisplayConfiguration (line 83) | type DisplayConfiguration struct
function DisplayFromUrl (line 91) | func DisplayFromUrl(path string, query map[string][]string) (*DisplayCon...
method NextDisplayId (line 156) | func (arena *Arena) NextDisplayId() string {
method RegisterDisplay (line 172) | func (arena *Arena) RegisterDisplay(displayConfig *DisplayConfiguration,...
method UpdateDisplay (line 202) | func (arena *Arena) UpdateDisplay(displayConfig DisplayConfiguration) er...
method MarkDisplayDisconnected (line 219) | func (arena *Arena) MarkDisplayDisconnected(displayId string) {
method purgeDisconnectedDisplays (line 238) | func (arena *Arena) purgeDisconnectedDisplays() {
FILE: field/display_test.go
function TestDisplayFromUrl (line 12) | func TestDisplayFromUrl(t *testing.T) {
function TestDisplayToUrl (line 54) | func TestDisplayToUrl(t *testing.T) {
function TestNextDisplayId (line 66) | func TestNextDisplayId(t *testing.T) {
function TestDisplayRegisterUnregister (line 76) | func TestDisplayRegisterUnregister(t *testing.T) {
function TestDisplayUpdateError (line 142) | func TestDisplayUpdateError(t *testing.T) {
function TestDisplayPurge (line 152) | func TestDisplayPurge(t *testing.T) {
FILE: field/driver_station_connection.go
constant driverStationTcpListenPort (line 24) | driverStationTcpListenPort = 1750
constant driverStationUdpSendPort (line 25) | driverStationUdpSendPort = 1121
constant driverStationUdpSendPortLite (line 26) | driverStationUdpSendPortLite = 1120
constant driverStationUdpReceivePort (line 27) | driverStationUdpReceivePort = 1160
constant driverStationTcpLinkTimeoutSec (line 28) | driverStationTcpLinkTimeoutSec = 5
constant driverStationUdpLinkTimeoutSec (line 29) | driverStationUdpLinkTimeoutSec = 1
constant maxTcpPacketBytes (line 30) | maxTcpPacketBytes = 65537
type DriverStationConnection (line 33) | type DriverStationConnection struct
method update (line 183) | func (dsConn *DriverStationConnection) update(arena *Arena, gameData s...
method close (line 201) | func (dsConn *DriverStationConnection) close() {
method signalMatchStart (line 214) | func (dsConn *DriverStationConnection) signalMatchStart(match *model.M...
method encodeControlPacket (line 223) | func (dsConn *DriverStationConnection) encodeControlPacket(arena *Aren...
method sendControlPacket (line 310) | func (dsConn *DriverStationConnection) sendControlPacket(arena *Arena,...
method handleTcpConnection (line 477) | func (dsConn *DriverStationConnection) handleTcpConnection(arena *Aren...
method checkGameData (line 535) | func (dsConn *DriverStationConnection) checkGameData(gameData string) ...
method sendGameDataPacket (line 550) | func (dsConn *DriverStationConnection) sendGameDataPacket(gameData str...
function driverStationTeamIdFromRemoteAddr (line 64) | func driverStationTeamIdFromRemoteAddr(addr net.Addr) (int, string, bool) {
function newDriverStationConnection (line 80) | func newDriverStationConnection(
method listenForDsUdpPackets (line 110) | func (arena *Arena) listenForDsUdpPackets() {
method listenForDriverStations (line 324) | func (arena *Arena) listenForDriverStations() {
method serveDriverStations (line 336) | func (arena *Arena) serveDriverStations(listener net.Listener) {
function readTaggedTcpPacket (line 452) | func readTaggedTcpPacket(tcpConn net.Conn, buffer []byte) (int, error) {
function handleInvalidTcpConnection (line 508) | func handleInvalidTcpConnection(tcpConn net.Conn, status int, station in...
FILE: field/driver_station_connection_test.go
function TestEncodeControlPacket (line 16) | func TestEncodeControlPacket(t *testing.T) {
function TestSendControlPacket (line 138) | func TestSendControlPacket(t *testing.T) {
function TestListenForDriverStations (line 152) | func TestListenForDriverStations(t *testing.T) {
function TestListenForDriverStations_NetworkSecurityIgnoresNonFieldIp (line 235) | func TestListenForDriverStations_NetworkSecurityIgnoresNonFieldIp(t *tes...
function TestNewDriverStationConnection_UdpPortSelection (line 260) | func TestNewDriverStationConnection_UdpPortSelection(t *testing.T) {
function setupFakeTcpConnection (line 280) | func setupFakeTcpConnection(t *testing.T) net.Conn {
function startTestDriverStationServer (line 290) | func startTestDriverStationServer(t *testing.T, arena *Arena) string {
function waitForDriverStationConnection (line 301) | func waitForDriverStationConnection(t *testing.T, arena *Arena, station ...
FILE: field/event_status.go
constant maxExpectedCycleTimeSec (line 15) | maxExpectedCycleTimeSec = 900
type EventStatus (line 17) | type EventStatus struct
method updateCycleTime (line 25) | func (arena *Arena) updateCycleTime(matchStartTime time.Time) {
method updateEarlyLateMessage (line 60) | func (arena *Arena) updateEarlyLateMessage() {
method getEarlyLateMessage (line 69) | func (arena *Arena) getEarlyLateMessage() string {
FILE: field/event_status_test.go
function TestCycleTime (line 14) | func TestCycleTime(t *testing.T) {
function TestCycleTimeDelta (line 38) | func TestCycleTimeDelta(t *testing.T) {
function TestEarlyLateMessage (line 72) | func TestEarlyLateMessage(t *testing.T) {
function setMatch (line 158) | func setMatch(database *model.Database, match *model.Match, matchTime ti...
FILE: field/fake_plc_test.go
type FakePlc (line 12) | type FakePlc struct
method SetAddress (line 31) | func (plc *FakePlc) SetAddress(address string) {
method IsEnabled (line 34) | func (plc *FakePlc) IsEnabled() bool {
method IsHealthy (line 38) | func (plc *FakePlc) IsHealthy() bool {
method IoChangeNotifier (line 42) | func (plc *FakePlc) IoChangeNotifier() *websocket.Notifier {
method Run (line 46) | func (plc *FakePlc) Run() {
method GetArmorBlockStatuses (line 49) | func (plc *FakePlc) GetArmorBlockStatuses() map[string]bool {
method GetFieldEStop (line 53) | func (plc *FakePlc) GetFieldEStop() bool {
method GetTeamEStops (line 57) | func (plc *FakePlc) GetTeamEStops() ([3]bool, [3]bool) {
method GetTeamAStops (line 61) | func (plc *FakePlc) GetTeamAStops() ([3]bool, [3]bool) {
method GetEthernetConnected (line 65) | func (plc *FakePlc) GetEthernetConnected() ([3]bool, [3]bool) {
method ResetMatch (line 69) | func (plc *FakePlc) ResetMatch() {
method SetStackLights (line 72) | func (plc *FakePlc) SetStackLights(red, blue, orange, green bool) {
method SetStackBuzzer (line 79) | func (plc *FakePlc) SetStackBuzzer(state bool) {
method SetFieldResetLight (line 83) | func (plc *FakePlc) SetFieldResetLight(state bool) {
method GetCycleState (line 87) | func (plc *FakePlc) GetCycleState(max, index, duration int) bool {
method GetInputNames (line 91) | func (plc *FakePlc) GetInputNames() []string {
method GetRegisterNames (line 95) | func (plc *FakePlc) GetRegisterNames() []string {
method GetCoilNames (line 99) | func (plc *FakePlc) GetCoilNames() []string {
method GetProcessorCounts (line 103) | func (plc *FakePlc) GetProcessorCounts() (int, int) {
method SetTrussLights (line 107) | func (plc *FakePlc) SetTrussLights(redLights, blueLights [3]bool) {
FILE: field/realtime_score.go
type RealtimeScore (line 10) | type RealtimeScore struct
function NewRealtimeScore (line 16) | func NewRealtimeScore() *RealtimeScore {
FILE: field/scoring_panel_registry.go
type ScoringPanelRegistry (line 13) | type ScoringPanelRegistry struct
method initialize (line 18) | func (registry *ScoringPanelRegistry) initialize() {
method resetScoreCommitted (line 23) | func (registry *ScoringPanelRegistry) resetScoreCommitted() {
method GetNumPanels (line 35) | func (registry *ScoringPanelRegistry) GetNumPanels(position string) int {
method GetNumScoreCommitted (line 43) | func (registry *ScoringPanelRegistry) GetNumScoreCommitted(position st...
method RegisterPanel (line 57) | func (registry *ScoringPanelRegistry) RegisterPanel(position string, w...
method SetScoreCommitted (line 68) | func (registry *ScoringPanelRegistry) SetScoreCommitted(position strin...
method UnregisterPanel (line 76) | func (registry *ScoringPanelRegistry) UnregisterPanel(position string,...
FILE: field/scoring_panel_registry_test.go
function TestScoringPanelRegistry (line 12) | func TestScoringPanelRegistry(t *testing.T) {
FILE: field/team_match_log.go
constant logsDir (line 19) | logsDir = "static/logs"
type TeamMatchLog (line 21) | type TeamMatchLog struct
method LogDsPacket (line 58) | func (log *TeamMatchLog) LogDsPacket(matchTimeSec float64, packetType ...
method Close (line 82) | func (log *TeamMatchLog) Close() {
function NewTeamMatchLog (line 28) | func NewTeamMatchLog(teamId int, match *model.Match, wifiStatus *network...
FILE: field/team_sign.go
type TeamSigns (line 21) | type TeamSigns struct
method Update (line 82) | func (signs *TeamSigns) Update(arena *Arena) {
method SetNextMatchTeams (line 126) | func (signs *TeamSigns) SetNextMatchTeams(teams [6]int) {
type TeamSign (line 33) | type TeamSign struct
method SetId (line 136) | func (sign *TeamSign) SetId(id int) {
method update (line 167) | func (sign *TeamSign) update(
method generateTeamNumberTexts (line 272) | func (sign *TeamSign) generateTeamNumberTexts(
method sendPacket (line 357) | func (sign *TeamSign) sendPacket() error {
method writePacketData (line 402) | func (sign *TeamSign) writePacketData(data []byte) {
constant teamSignAddressPrefix (line 50) | teamSignAddressPrefix = "10.0.100."
constant teamSignYear (line 51) | teamSignYear = 2025
constant teamSignPort (line 52) | teamSignPort = 10011
constant teamSignPacketMagicString (line 53) | teamSignPacketMagicString = "CYPRX"
constant teamSignPacketHeaderLength (line 54) | teamSignPacketHeaderLength = 7
constant teamSignCommandSetDisplay (line 55) | teamSignCommandSetDisplay = 0x04
constant teamSignAddressSingle (line 56) | teamSignAddressSingle = 0x01
constant teamSignPacketTypeFrontText (line 57) | teamSignPacketTypeFrontText = 0x01
constant teamSignPacketTypeRearText (line 58) | teamSignPacketTypeRearText = 0x02
constant teamSignPacketTypeFrontIntensity (line 59) | teamSignPacketTypeFrontIntensity = 0x03
constant teamSignPacketTypeColor (line 60) | teamSignPacketTypeColor = 0x04
constant teamSignPacketPeriodMs (line 61) | teamSignPacketPeriodMs = 5000
constant teamSignBlinkPeriodMs (line 62) | teamSignBlinkPeriodMs = 750
function NewTeamSigns (line 74) | func NewTeamSigns() *TeamSigns {
function generateInMatchTeamRearText (line 189) | func generateInMatchTeamRearText(arena *Arena, isRed bool, countdown str...
function generateInMatchTimerRearText (line 216) | func generateInMatchTimerRearText(arena *Arena, isRed bool) string {
function generateTimerTexts (line 234) | func generateTimerTexts(arena *Arena, countdown, inMatchRearText string)...
function blinkColor (line 410) | func blinkColor(originalColor color.RGBA) color.RGBA {
FILE: field/team_sign_test.go
function TestTeamSign_GenerateInMatchRearText (line 14) | func TestTeamSign_GenerateInMatchRearText(t *testing.T) {
function TestTeamSign_Timer (line 36) | func TestTeamSign_Timer(t *testing.T) {
function TestTeamSign_TeamNumber (line 88) | func TestTeamSign_TeamNumber(t *testing.T) {
FILE: field/test_helpers.go
function SetupTestArena (line 17) | func SetupTestArena(t *testing.T) *Arena {
function setupTestArena (line 32) | func setupTestArena(t *testing.T) *Arena {
FILE: game/foul.go
type Foul (line 8) | type Foul struct
method Rule (line 16) | func (foul *Foul) Rule() *Rule {
method PointValue (line 21) | func (foul *Foul) PointValue() int {
FILE: game/match_sounds.go
type MatchSound (line 8) | type MatchSound struct
function UpdateMatchSounds (line 18) | func UpdateMatchSounds() {
FILE: game/match_timing.go
constant TeleopGracePeriodSec (line 11) | TeleopGracePeriodSec = 3
function GetDurationToAutoEnd (line 23) | func GetDurationToAutoEnd() time.Duration {
function GetDurationToTeleopStart (line 27) | func GetDurationToTeleopStart() time.Duration {
function GetDurationToTeleopEnd (line 33) | func GetDurationToTeleopEnd() time.Duration {
FILE: game/ranking_fields.go
type RankingFields (line 10) | type RankingFields struct
method AddScoreSummary (line 33) | func (fields *RankingFields) AddScoreSummary(ownScore *ScoreSummary, o...
type Ranking (line 24) | type Ranking struct
type Rankings (line 31) | type Rankings
method Len (line 67) | func (rankings Rankings) Len() int {
method Less (line 72) | func (rankings Rankings) Less(i, j int) bool {
method Swap (line 96) | func (rankings Rankings) Swap(i, j int) {
FILE: game/ranking_fields_test.go
function TestAddScoreSummary (line 13) | func TestAddScoreSummary(t *testing.T) {
function TestSortRankings (line 58) | func TestSortRankings(t *testing.T) {
FILE: game/reef.go
type Reef (line 8) | type Reef struct
method CountTotalCoralByLevel (line 42) | func (reef *Reef) CountTotalCoralByLevel(level Level) int {
method AutoCoralCount (line 47) | func (reef *Reef) AutoCoralCount() int {
method AutoCoralPoints (line 57) | func (reef *Reef) AutoCoralPoints() int {
method TeleopCoralCount (line 66) | func (reef *Reef) TeleopCoralCount() int {
method TeleopCoralPoints (line 76) | func (reef *Reef) TeleopCoralPoints() int {
method CountCoralByLevelAndPeriod (line 85) | func (reef *Reef) CountCoralByLevelAndPeriod(level Level, isAuto bool)...
method isAutoBonusCoralThresholdMet (line 116) | func (reef *Reef) isAutoBonusCoralThresholdMet() bool {
method countCoralBonusSatisfiedLevels (line 131) | func (reef *Reef) countCoralBonusSatisfiedLevels() int {
type Level (line 17) | type Level
constant Level1 (line 20) | Level1 Level = iota - 1
constant Level2 (line 21) | Level2
constant Level3 (line 22) | Level3
constant Level4 (line 23) | Level4
constant LevelCount (line 24) | LevelCount
FILE: game/reef_test.go
function TestReefCoralCountsAndPoints (line 12) | func TestReefCoralCountsAndPoints(t *testing.T) {
function TestReef_isAutoBonusCoralThresholdMet (line 145) | func TestReef_isAutoBonusCoralThresholdMet(t *testing.T) {
function TestReef_countCoralBonusSatisfiedLevels (line 222) | func TestReef_countCoralBonusSatisfiedLevels(t *testing.T) {
FILE: game/rule.go
type Rule (line 8) | type Rule struct
function GetRuleById (line 65) | func GetRuleById(id int) *Rule {
function GetAllRules (line 70) | func GetAllRules() map[int]*Rule {
FILE: game/rule_test.go
function TestGetRuleById (line 11) | func TestGetRuleById(t *testing.T) {
function TestGetAllRules (line 18) | func TestGetAllRules(t *testing.T) {
FILE: game/score.go
type Score (line 8) | type Score struct
method Summarize (line 37) | func (score *Score) Summarize(opponentScore *Score) *ScoreSummary {
method Equals (line 160) | func (score *Score) Equals(other *Score) bool {
type EndgameStatus (line 27) | type EndgameStatus
constant EndgameNone (line 30) | EndgameNone EndgameStatus = iota
constant EndgameParked (line 31) | EndgameParked
constant EndgameShallowCage (line 32) | EndgameShallowCage
constant EndgameDeepCage (line 33) | EndgameDeepCage
FILE: game/score_summary.go
type ScoreSummary (line 8) | type ScoreSummary struct
type MatchStatus (line 30) | type MatchStatus
method Get (line 40) | func (t MatchStatus) Get() MatchStatus {
constant MatchScheduled (line 33) | MatchScheduled MatchStatus = iota
constant MatchHidden (line 34) | MatchHidden
constant RedWonMatch (line 35) | RedWonMatch
constant BlueWonMatch (line 36) | BlueWonMatch
constant TieMatch (line 37) | TieMatch
function DetermineMatchStatus (line 45) | func DetermineMatchStatus(redScoreSummary, blueScoreSummary *ScoreSummar...
function comparePoints (line 69) | func comparePoints(redPoints, bluePoints int) MatchStatus {
FILE: game/score_summary_test.go
function TestScoreSummaryDetermineMatchStatus (line 11) | func TestScoreSummaryDetermineMatchStatus(t *testing.T) {
FILE: game/score_test.go
function TestScoreSummary (line 12) | func TestScoreSummary(t *testing.T) {
function TestScoreAutoBonusRankingPoint (line 71) | func TestScoreAutoBonusRankingPoint(t *testing.T) {
function TestScoreCoralBonusRankingPoint (line 116) | func TestScoreCoralBonusRankingPoint(t *testing.T) {
function TestScoreBargeBonusRankingPoint (line 190) | func TestScoreBargeBonusRankingPoint(t *testing.T) {
function TestScoreBargeBonusRankingPointIncludingAlgae (line 281) | func TestScoreBargeBonusRankingPointIncludingAlgae(t *testing.T) {
function TestScoreAutoRankingPointFromFouls (line 304) | func TestScoreAutoRankingPointFromFouls(t *testing.T) {
function TestScoreEquals (line 384) | func TestScoreEquals(t *testing.T) {
FILE: game/test_helpers.go
function TestScore1 (line 8) | func TestScore1() *Score {
function TestScore2 (line 37) | func TestScore2() *Score {
function TestRanking1 (line 57) | func TestRanking1() *Ranking {
function TestRanking2 (line 61) | func TestRanking2() *Ranking {
FILE: main.go
constant eventDbPath (line 17) | eventDbPath = "./event.db"
constant httpPort (line 18) | httpPort = 8080
function main (line 21) | func main() {
FILE: model/alliance.go
type Alliance (line 10) | type Alliance struct
type AllianceSelectionRankedTeam (line 16) | type AllianceSelectionRankedTeam struct
method CreateAlliance (line 22) | func (database *Database) CreateAlliance(alliance *Alliance) error {
method GetAllianceById (line 26) | func (database *Database) GetAllianceById(id int) (*Alliance, error) {
method UpdateAlliance (line 30) | func (database *Database) UpdateAlliance(alliance *Alliance) error {
method DeleteAlliance (line 34) | func (database *Database) DeleteAlliance(id int) error {
method TruncateAlliances (line 38) | func (database *Database) TruncateAlliances() error {
method GetAllAlliances (line 42) | func (database *Database) GetAllAlliances() ([]Alliance, error) {
method UpdateAllianceFromMatch (line 57) | func (database *Database) UpdateAllianceFromMatch(allianceId int, matchT...
method GetOffFieldTeamIds (line 92) | func (database *Database) GetOffFieldTeamIds(match *Match) ([]int, []int...
method getOffFieldTeamIdsForAlliance (line 110) | func (database *Database) getOffFieldTeamIdsForAlliance(allianceId int, ...
FILE: model/alliance_test.go
function TestGetNonexistentAlliance (line 11) | func TestGetNonexistentAlliance(t *testing.T) {
function TestAllianceCrud (line 20) | func TestAllianceCrud(t *testing.T) {
function TestUpdateAllianceFromMatch (line 42) | func TestUpdateAllianceFromMatch(t *testing.T) {
function TestTruncateAllianceTeams (line 55) | func TestTruncateAllianceTeams(t *testing.T) {
function TestGetAllAlliances (line 67) | func TestGetAllAlliances(t *testing.T) {
function TestGetOffFieldTeamIds (line 86) | func TestGetOffFieldTeamIds(t *testing.T) {
FILE: model/award.go
type Award (line 10) | type Award struct
type AwardType (line 18) | type AwardType
constant JudgedAward (line 21) | JudgedAward AwardType = iota
constant FinalistAward (line 22) | FinalistAward
constant WinnerAward (line 23) | WinnerAward
method CreateAward (line 26) | func (database *Database) CreateAward(award *Award) error {
method GetAwardById (line 30) | func (database *Database) GetAwardById(id int) (*Award, error) {
method UpdateAward (line 34) | func (database *Database) UpdateAward(award *Award) error {
method DeleteAward (line 38) | func (database *Database) DeleteAward(id int) error {
method TruncateAwards (line 42) | func (database *Database) TruncateAwards() error {
method GetAllAwards (line 46) | func (database *Database) GetAllAwards() ([]Award, error) {
method GetAwardsByType (line 60) | func (database *Database) GetAwardsByType(awardType AwardType) ([]Award,...
FILE: model/award_test.go
function TestGetNonexistentAward (line 11) | func TestGetNonexistentAward(t *testing.T) {
function TestAwardCrud (line 20) | func TestAwardCrud(t *testing.T) {
function TestTruncateAwards (line 54) | func TestTruncateAwards(t *testing.T) {
function TestGetAwardsByType (line 66) | func TestGetAwardsByType(t *testing.T) {
FILE: model/database.go
constant backupsDir (line 19) | backupsDir = "db/backups"
type Database (line 23) | type Database struct
method Close (line 94) | func (database *Database) Close() error {
method Backup (line 99) | func (database *Database) Backup(eventName, reason string) error {
method WriteBackup (line 126) | func (database *Database) WriteBackup(writer io.Writer) error {
function OpenDatabase (line 42) | func OpenDatabase(filename string) (*Database, error) {
FILE: model/database_test.go
function TestOpenUnreachableDatabase (line 11) | func TestOpenUnreachableDatabase(t *testing.T) {
function setupTestDb (line 16) | func setupTestDb(t *testing.T) *Database {
FILE: model/event_settings.go
type PlayoffType (line 14) | type PlayoffType
constant DoubleEliminationPlayoff (line 17) | DoubleEliminationPlayoff PlayoffType = iota
constant SingleEliminationPlayoff (line 18) | SingleEliminationPlayoff
type EventSettings (line 41) | type EventSettings struct
method GetEventSettings (line 121) | func (database *Database) GetEventSettings() (*EventSettings, error) {
method UpdateEventSettings (line 161) | func (database *Database) UpdateEventSettings(eventSettings *EventSettin...
FILE: model/event_settings_test.go
function TestEventSettingsReadWrite (line 11) | func TestEventSettingsReadWrite(t *testing.T) {
FILE: model/judging_slot.go
type JudgingSlot (line 13) | type JudgingSlot struct
method CreateJudgingSlot (line 24) | func (database *Database) CreateJudgingSlot(judgingSlot *JudgingSlot) er...
method TruncateJudgingSlots (line 28) | func (database *Database) TruncateJudgingSlots() error {
method GetAllJudgingSlots (line 32) | func (database *Database) GetAllJudgingSlots() ([]JudgingSlot, error) {
FILE: model/judging_slot_test.go
function TestJudgingSlotCrud (line 12) | func TestJudgingSlotCrud(t *testing.T) {
FILE: model/lower_third.go
type LowerThird (line 12) | type LowerThird struct
method CreateLowerThird (line 20) | func (database *Database) CreateLowerThird(lowerThird *LowerThird) error {
method GetLowerThirdById (line 24) | func (database *Database) GetLowerThirdById(id int) (*LowerThird, error) {
method UpdateLowerThird (line 28) | func (database *Database) UpdateLowerThird(lowerThird *LowerThird) error {
method DeleteLowerThird (line 32) | func (database *Database) DeleteLowerThird(id int) error {
method TruncateLowerThirds (line 36) | func (database *Database) TruncateLowerThirds() error {
method GetAllLowerThirds (line 40) | func (database *Database) GetAllLowerThirds() ([]LowerThird, error) {
method GetLowerThirdsByAwardId (line 54) | func (database *Database) GetLowerThirdsByAwardId(awardId int) ([]LowerT...
method GetNextLowerThirdDisplayOrder (line 69) | func (database *Database) GetNextLowerThirdDisplayOrder() int {
FILE: model/lower_third_test.go
function TestGetNonexistentLowerThird (line 11) | func TestGetNonexistentLowerThird(t *testing.T) {
function TestLowerThirdCrud (line 20) | func TestLowerThirdCrud(t *testing.T) {
function TestTruncateLowerThirds (line 50) | func TestTruncateLowerThirds(t *testing.T) {
function TestGetLowerThirdsByAwardId (line 62) | func TestGetLowerThirdsByAwardId(t *testing.T) {
FILE: model/match.go
type MatchType (line 17) | type MatchType
method Get (line 26) | func (t MatchType) Get() MatchType {
constant Test (line 20) | Test MatchType = iota
constant Practice (line 21) | Practice
constant Qualification (line 22) | Qualification
constant Playoff (line 23) | Playoff
type Match (line 30) | type Match struct
method IsComplete (line 123) | func (match *Match) IsComplete() bool {
method ShouldAllowSubstitution (line 128) | func (match *Match) ShouldAllowSubstitution() bool {
method ShouldAllowNexusSubstitution (line 133) | func (match *Match) ShouldAllowNexusSubstitution() bool {
method ShouldUpdateCards (line 138) | func (match *Match) ShouldUpdateCards() bool {
method ShouldUpdateRankings (line 143) | func (match *Match) ShouldUpdateRankings() bool {
method ShouldUpdatePlayoffMatches (line 148) | func (match *Match) ShouldUpdatePlayoffMatches() bool {
type TbaMatchKey (line 61) | type TbaMatchKey struct
method String (line 168) | func (key TbaMatchKey) String() string {
method CreateMatch (line 67) | func (database *Database) CreateMatch(match *Match) error {
method GetMatchById (line 71) | func (database *Database) GetMatchById(id int) (*Match, error) {
method UpdateMatch (line 75) | func (database *Database) UpdateMatch(match *Match) error {
method DeleteMatch (line 79) | func (database *Database) DeleteMatch(id int) error {
method TruncateMatches (line 83) | func (database *Database) TruncateMatches() error {
method GetMatchByTypeOrder (line 87) | func (database *Database) GetMatchByTypeOrder(matchType MatchType, typeO...
method GetMatchesByType (line 101) | func (database *Database) GetMatchesByType(matchType MatchType, includeH...
function MatchTypeFromString (line 153) | func MatchTypeFromString(matchTypeString string) (MatchType, error) {
FILE: model/match_result.go
type MatchResult (line 12) | type MatchResult struct
method RedScoreSummary (line 66) | func (matchResult *MatchResult) RedScoreSummary() *game.ScoreSummary {
method BlueScoreSummary (line 71) | func (matchResult *MatchResult) BlueScoreSummary() *game.ScoreSummary {
method CorrectPlayoffScore (line 76) | func (matchResult *MatchResult) CorrectPlayoffScore() {
function NewMatchResult (line 24) | func NewMatchResult() *MatchResult {
method CreateMatchResult (line 33) | func (database *Database) CreateMatchResult(matchResult *MatchResult) er...
method GetMatchResultForMatch (line 37) | func (database *Database) GetMatchResultForMatch(matchId int) (*MatchRes...
method UpdateMatchResult (line 53) | func (database *Database) UpdateMatchResult(matchResult *MatchResult) er...
method DeleteMatchResult (line 57) | func (database *Database) DeleteMatchResult(id int) error {
method TruncateMatchResults (line 61) | func (database *Database) TruncateMatchResults() error {
FILE: model/match_result_test.go
function TestGetNonexistentMatchResult (line 12) | func TestGetNonexistentMatchResult(t *testing.T) {
function TestMatchResultCrud (line 21) | func TestMatchResultCrud(t *testing.T) {
function TestTruncateMatchResults (line 44) | func TestTruncateMatchResults(t *testing.T) {
function TestGetMatchResultForMatch (line 56) | func TestGetMatchResultForMatch(t *testing.T) {
FILE: model/match_test.go
function TestGetNonexistentMatch (line 13) | func TestGetNonexistentMatch(t *testing.T) {
function TestMatchCrud (line 22) | func TestMatchCrud(t *testing.T) {
function TestTruncateMatches (line 62) | func TestTruncateMatches(t *testing.T) {
function TestGetMatchByTypeOrder (line 85) | func TestGetMatchByTypeOrder(t *testing.T) {
function TestGetMatchesByType (line 115) | func TestGetMatchesByType(t *testing.T) {
function TestMatchTypeFromString (line 169) | func TestMatchTypeFromString(t *testing.T) {
function TestTbaMatchKeyString (line 201) | func TestTbaMatchKeyString(t *testing.T) {
FILE: model/matchtype_string.go
function _ (line 7) | func _() {
constant _MatchType_name (line 17) | _MatchType_name = "TestPracticeQualificationPlayoff"
method String (line 21) | func (i MatchType) String() string {
FILE: model/ranking.go
method CreateRanking (line 13) | func (database *Database) CreateRanking(ranking *game.Ranking) error {
method GetRankingForTeam (line 17) | func (database *Database) GetRankingForTeam(teamId int) (*game.Ranking, ...
method UpdateRanking (line 21) | func (database *Database) UpdateRanking(ranking *game.Ranking) error {
method DeleteRanking (line 25) | func (database *Database) DeleteRanking(teamId int) error {
method TruncateRankings (line 29) | func (database *Database) TruncateRankings() error {
method GetAllRankings (line 33) | func (database *Database) GetAllRankings() (game.Rankings, error) {
method ReplaceAllRankings (line 48) | func (database *Database) ReplaceAllRankings(rankings game.Rankings) err...
FILE: model/ranking_test.go
function TestGetNonexistentRanking (line 12) | func TestGetNonexistentRanking(t *testing.T) {
function TestRankingCrud (line 21) | func TestRankingCrud(t *testing.T) {
function TestTruncateRankings (line 38) | func TestTruncateRankings(t *testing.T) {
function TestGetAllRankings (line 50) | func TestGetAllRankings(t *testing.T) {
FILE: model/schedule_block.go
type ScheduleBlock (line 13) | type ScheduleBlock struct
method CreateScheduleBlock (line 21) | func (database *Database) CreateScheduleBlock(block *ScheduleBlock) error {
method GetScheduleBlocksByMatchType (line 25) | func (database *Database) GetScheduleBlocksByMatchType(matchType MatchTy...
method DeleteScheduleBlocksByMatchType (line 47) | func (database *Database) DeleteScheduleBlocksByMatchType(matchType Matc...
method TruncateScheduleBlocks (line 61) | func (database *Database) TruncateScheduleBlocks() error {
FILE: model/schedule_block_test.go
function TestScheduleBlockCrud (line 12) | func TestScheduleBlockCrud(t *testing.T) {
FILE: model/scheduled_break.go
type ScheduledBreak (line 13) | type ScheduledBreak struct
method CreateScheduledBreak (line 22) | func (database *Database) CreateScheduledBreak(scheduledBreak *Scheduled...
method GetScheduledBreakById (line 26) | func (database *Database) GetScheduledBreakById(id int) (*ScheduledBreak...
method UpdateScheduledBreak (line 30) | func (database *Database) UpdateScheduledBreak(scheduledBreak *Scheduled...
method GetScheduledBreaksByMatchType (line 34) | func (database *Database) GetScheduledBreaksByMatchType(matchType MatchT...
method GetScheduledBreakByMatchTypeOrder (line 56) | func (database *Database) GetScheduledBreakByMatchTypeOrder(
method DeleteScheduledBreaksByMatchType (line 73) | func (database *Database) DeleteScheduledBreaksByMatchType(matchType Mat...
method TruncateScheduledBreaks (line 87) | func (database *Database) TruncateScheduledBreaks() error {
FILE: model/scheduled_break_test.go
function TestScheduledBreakCrud (line 12) | func TestScheduledBreakCrud(t *testing.T) {
FILE: model/sponsor_slide.go
type SponsorSlide (line 10) | type SponsorSlide struct
method CreateSponsorSlide (line 20) | func (database *Database) CreateSponsorSlide(sponsorSlide *SponsorSlide)...
method GetSponsorSlideById (line 24) | func (database *Database) GetSponsorSlideById(id int) (*SponsorSlide, er...
method UpdateSponsorSlide (line 28) | func (database *Database) UpdateSponsorSlide(sponsorSlide *SponsorSlide)...
method DeleteSponsorSlide (line 32) | func (database *Database) DeleteSponsorSlide(id int) error {
method TruncateSponsorSlides (line 36) | func (database *Database) TruncateSponsorSlides() error {
method GetAllSponsorSlides (line 40) | func (database *Database) GetAllSponsorSlides() ([]SponsorSlide, error) {
method GetNextSponsorSlideDisplayOrder (line 54) | func (database *Database) GetNextSponsorSlideDisplayOrder() int {
FILE: model/sponsor_slide_test.go
function TestGetNonexistentSponsorSlide (line 11) | func TestGetNonexistentSponsorSlide(t *testing.T) {
function TestSponsorSlideCrud (line 20) | func TestSponsorSlideCrud(t *testing.T) {
function TestTruncateSponsorSlides (line 45) | func TestTruncateSponsorSlides(t *testing.T) {
FILE: model/table.go
type table (line 18) | type table struct
function newTable (line 28) | func newTable[R any](database *Database) (*table[R], error) {
method getById (line 82) | func (table *table[R]) getById(id int) (*R, error) {
method getAll (line 104) | func (table *table[R]) getAll() ([]R, error) {
method create (line 130) | func (table *table[R]) create(record *R) error {
method update (line 178) | func (table *table[R]) update(record *R) error {
method delete (line 210) | func (table *table[R]) delete(id int) error {
method truncate (line 231) | func (table *table[R]) truncate() error {
method getBucket (line 251) | func (table *table[R]) getBucket(tx *bbolt.Tx) (*bbolt.Bucket, error) {
function idToKey (line 260) | func idToKey(id int) []byte {
FILE: model/table_test.go
type validRecord (line 11) | type validRecord struct
type manualIdRecord (line 17) | type manualIdRecord struct
function TestTableSingleCrud (line 22) | func TestTableSingleCrud(t *testing.T) {
function TestTableMultipleCrud (line 55) | func TestTableMultipleCrud(t *testing.T) {
function TestTableWithManualId (line 91) | func TestTableWithManualId(t *testing.T) {
function TestNewTableErrors (line 132) | func TestNewTableErrors(t *testing.T) {
function TestTableCrudErrors (line 166) | func TestTableCrudErrors(t *testing.T) {
FILE: model/team.go
type Team (line 10) | type Team struct
method CreateTeam (line 27) | func (database *Database) CreateTeam(team *Team) error {
method GetTeamById (line 31) | func (database *Database) GetTeamById(id int) (*Team, error) {
method UpdateTeam (line 35) | func (database *Database) UpdateTeam(team *Team) error {
method DeleteTeam (line 39) | func (database *Database) DeleteTeam(id int) error {
method TruncateTeams (line 43) | func (database *Database) TruncateTeams() error {
method GetAllTeams (line 47) | func (database *Database) GetAllTeams() ([]Team, error) {
FILE: model/team_test.go
function TestGetNonexistentTeam (line 11) | func TestGetNonexistentTeam(t *testing.T) {
function TestTeamCrud (line 20) | func TestTeamCrud(t *testing.T) {
function TestTruncateTeams (line 51) | func TestTruncateTeams(t *testing.T) {
function TestGetAllTeams (line 72) | func TestGetAllTeams(t *testing.T) {
FILE: model/test_helpers.go
function SetupTestDb (line 15) | func SetupTestDb(t *testing.T) *Database {
function BuildTestMatchResult (line 29) | func BuildTestMatchResult(matchId int, playNumber int) *MatchResult {
function BuildTestAlliances (line 38) | func BuildTestAlliances(database *Database) {
FILE: model/user_session.go
type UserSession (line 10) | type UserSession struct
method CreateUserSession (line 17) | func (database *Database) CreateUserSession(session *UserSession) error {
method GetUserSessionByToken (line 21) | func (database *Database) GetUserSessionByToken(token string) (*UserSess...
method DeleteUserSession (line 35) | func (database *Database) DeleteUserSession(id int) error {
method TruncateUserSessions (line 39) | func (database *Database) TruncateUserSessions() error {
FILE: model/user_session_test.go
function TestGetNonexistentUserSession (line 12) | func TestGetNonexistentUserSession(t *testing.T) {
function TestUserSessionCrud (line 21) | func TestUserSessionCrud(t *testing.T) {
function TestTruncateUserSessions (line 40) | func TestTruncateUserSessions(t *testing.T) {
FILE: network/access_point.go
constant accessPointPollPeriodSec (line 22) | accessPointPollPeriodSec = 1
type AccessPoint (line 25) | type AccessPoint struct
method SetSettings (line 80) | func (ap *AccessPoint) SetSettings(
method Run (line 95) | func (ap *AccessPoint) Run() {
method ConfigureTeamWifi (line 114) | func (ap *AccessPoint) ConfigureTeamWifi(teams [6]*model.Team) error {
method updateMonitoring (line 161) | func (ap *AccessPoint) updateMonitoring() error {
method statusMatchesLastConfiguration (line 212) | func (ap *AccessPoint) statusMatchesLastConfiguration() bool {
type TeamWifiStatus (line 35) | type TeamWifiStatus struct
type configurationRequest (line 45) | type configurationRequest struct
type stationConfiguration (line 50) | type stationConfiguration struct
type accessPointStatus (line 55) | type accessPointStatus struct
method toLogString (line 267) | func (apStatus *accessPointStatus) toLogString() string {
type stationStatus (line 61) | type stationStatus struct
function addStation (line 230) | func addStation(stationsConfigurations map[string]stationConfiguration, ...
function updateTeamWifiStatus (line 241) | func updateTeamWifiStatus(teamWifiStatus *TeamWifiStatus, stationStatus ...
FILE: network/access_point_test.go
function TestAccessPoint_ConfigureTeamWifi (line 15) | func TestAccessPoint_ConfigureTeamWifi(t *testing.T) {
function TestAccessPoint_updateMonitoring (line 90) | func TestAccessPoint_updateMonitoring(t *testing.T) {
function TestAccessPoint_statusMatchesLastConfiguration (line 167) | func TestAccessPoint_statusMatchesLastConfiguration(t *testing.T) {
FILE: network/sccswitch.go
constant sccSwitchConnectTimeoutSec (line 20) | sccSwitchConnectTimeoutSec = 5
constant sccSwitchConfigTimeoutSec (line 21) | sccSwitchConfigTimeoutSec = 5
constant sccSwitchSSHPort (line 22) | sccSwitchSSHPort = 22
type SCCSwitch (line 25) | type SCCSwitch struct
method SetTeamEthernetEnabled (line 52) | func (scc *SCCSwitch) SetTeamEthernetEnabled(enabled bool) error {
method runCommandSequence (line 80) | func (scc *SCCSwitch) runCommandSequence(commands []string) (string, e...
function NewSCCSwitch (line 38) | func NewSCCSwitch(address, username, password string, upCommands, downCo...
FILE: network/sccswitch_test.go
function TestConfigureSCC (line 20) | func TestConfigureSCC(t *testing.T) {
function mockSSHSwitch (line 60) | func mockSSHSwitch(t *testing.T, port int, username, password string, co...
FILE: network/switch.go
constant switchConfigBackoffDurationSec (line 19) | switchConfigBackoffDurationSec = 5
constant switchConfigPauseDurationSec (line 20) | switchConfigPauseDurationSec = 2
constant switchTeamGatewayAddress (line 21) | switchTeamGatewayAddress = 4
constant switchTelnetPort (line 22) | switchTelnetPort = 23
constant red1Vlan (line 26) | red1Vlan = 10
constant red2Vlan (line 27) | red2Vlan = 20
constant red3Vlan (line 28) | red3Vlan = 30
constant blue1Vlan (line 29) | blue1Vlan = 40
constant blue2Vlan (line 30) | blue2Vlan = 50
constant blue3Vlan (line 31) | blue3Vlan = 60
type Switch (line 34) | type Switch struct
method ConfigureTeamEthernet (line 60) | func (sw *Switch) ConfigureTeamEthernet(teams [6]*model.Team) error {
method runCommand (line 131) | func (sw *Switch) runCommand(command string) (string, error) {
method runConfigCommand (line 166) | func (sw *Switch) runConfigCommand(command string) (string, error) {
constant DefaultServerIpAddress (line 44) | DefaultServerIpAddress = "10.0.100.5"
function NewSwitch (line 48) | func NewSwitch(address, password string) *Switch {
FILE: network/switch_test.go
function TestConfigureSwitch (line 16) | func TestConfigureSwitch(t *testing.T) {
function mockTelnet (line 88) | func mockTelnet(t *testing.T, port int, command1 *string, command2 *stri...
FILE: partner/blackmagic.go
constant blackmagicPort (line 17) | blackmagicPort = 9993
constant blackmagicConnectTimeoutMs (line 18) | blackmagicConnectTimeoutMs = 100
constant blackmagicStopDelaySec (line 19) | blackmagicStopDelaySec = 10
type BlackmagicClient (line 22) | type BlackmagicClient struct
method StartRecording (line 39) | func (client *BlackmagicClient) StartRecording() {
method StopRecording (line 44) | func (client *BlackmagicClient) StopRecording() {
method sendCommand (line 50) | func (client *BlackmagicClient) sendCommand(command string) {
function NewBlackmagicClient (line 27) | func NewBlackmagicClient(addresses string) *BlackmagicClient {
FILE: partner/blackmagic_test.go
function TestNewBlackmagicClient (line 11) | func TestNewBlackmagicClient(t *testing.T) {
FILE: partner/companion.go
constant companionConnectTimeoutMs (line 16) | companionConnectTimeoutMs = 1000
type CompanionEvent (line 20) | type CompanionEvent
constant EventMatchPreview (line 23) | EventMatchPreview CompanionEvent = "matchPreview"
constant EventShowOverlay (line 24) | EventShowOverlay CompanionEvent = "showOverlay"
constant EventMatchStart (line 25) | EventMatchStart CompanionEvent = "matchStart"
constant EventTeleopStart (line 26) | EventTeleopStart CompanionEvent = "teleopStart"
constant EventEndgameStart (line 27) | EventEndgameStart CompanionEvent = "endgameStart"
constant EventMatchEnd (line 28) | EventMatchEnd CompanionEvent = "matchEnd"
constant EventShowFinalScore (line 29) | EventShowFinalScore CompanionEvent = "showFinalScore"
constant EventAllianceSelection (line 30) | EventAllianceSelection CompanionEvent = "allianceSelection"
constant EventMatchAbort (line 31) | EventMatchAbort CompanionEvent = "matchAbort"
type CompanionEventConfig (line 35) | type CompanionEventConfig struct
type CompanionClient (line 41) | type CompanionClient struct
method SendEvent (line 61) | func (client *CompanionClient) SendEvent(event CompanionEvent) {
method sendCommand (line 77) | func (client *CompanionClient) sendCommand(command string) {
method IsEnabled (line 97) | func (client *CompanionClient) IsEnabled() bool {
method GetEventConfig (line 102) | func (client *CompanionClient) GetEventConfig(event CompanionEvent) (C...
function NewCompanionClient (line 48) | func NewCompanionClient(
FILE: partner/companion_test.go
function TestNewCompanionClient (line 13) | func TestNewCompanionClient(t *testing.T) {
function TestCompanionClient_IsEnabled (line 31) | func TestCompanionClient_IsEnabled(t *testing.T) {
function TestCompanionClient_GetEventConfig (line 39) | func TestCompanionClient_GetEventConfig(t *testing.T) {
function TestCompanionClient_SendEvent_Disabled (line 59) | func TestCompanionClient_SendEvent_Disabled(t *testing.T) {
function TestCompanionClient_SendEvent_UnconfiguredEvent (line 67) | func TestCompanionClient_SendEvent_UnconfiguredEvent(t *testing.T) {
function TestCompanionClient_SendEvent_InvalidConfig (line 78) | func TestCompanionClient_SendEvent_InvalidConfig(t *testing.T) {
FILE: partner/nexus.go
constant nexusBaseUrl (line 17) | nexusBaseUrl = "https://frc.nexus"
constant nexusApiKey (line 18) | nexusApiKey = "Vn6D9y80kQcNijDItKOJHg8yYEk"
type NexusClient (line 20) | type NexusClient struct
method GetLineup (line 36) | func (client *NexusClient) GetLineup(tbaMatchKey model.TbaMatchKey) (*...
method getRequest (line 81) | func (client *NexusClient) getRequest(path string) (*http.Response, er...
type nexusLineup (line 26) | type nexusLineup struct
function NewNexusClient (line 31) | func NewNexusClient(eventCode string) *NexusClient {
FILE: partner/nexus_test.go
function TestGetLineup (line 15) | func TestGetLineup(t *testing.T) {
FILE: partner/tba.go
constant tbaBaseUrl (line 24) | tbaBaseUrl = "https://www.thebluealliance.com"
constant tbaAuthKey (line 25) | tbaAuthKey = "MAApv9MCuKY9MSFkXLuzTSYBCdosboxDq8Q3ujUE2Mn8PD3Nmv64uczu5L...
constant AvatarsDir (line 26) | AvatarsDir = "static/img/avatars"
type TbaClient (line 29) | type TbaClient struct
method GetTeam (line 175) | func (client *TbaClient) GetTeam(teamNumber int) (*TbaTeam, error) {
method GetRobotName (line 195) | func (client *TbaClient) GetRobotName(teamNumber int, year int) (strin...
method GetTeamAwards (line 222) | func (client *TbaClient) GetTeamAwards(teamNumber int) ([]*TbaAward, e...
method DownloadTeamAvatar (line 255) | func (client *TbaClient) DownloadTeamAvatar(teamNumber, year int) error {
method PublishTeams (line 296) | func (client *TbaClient) PublishTeams(database *model.Database) error {
method PublishMatches (line 325) | func (client *TbaClient) PublishMatches(database *model.Database) error {
method PublishRankings (line 406) | func (client *TbaClient) PublishRankings(database *model.Database) err...
method PublishAlliances (line 449) | func (client *TbaClient) PublishAlliances(database *model.Database) er...
method PublishAwards (line 500) | func (client *TbaClient) PublishAwards(database *model.Database) error {
method DeletePublishedMatches (line 531) | func (client *TbaClient) DeletePublishedMatches() error {
method getEventName (line 544) | func (client *TbaClient) getEventName(eventCode string) (string, error) {
method getRequest (line 573) | func (client *TbaClient) getRequest(path string) (*http.Response, erro...
method postRequest (line 587) | func (client *TbaClient) postRequest(resource string, action string, b...
type TbaMatch (line 37) | type TbaMatch struct
type TbaAlliance (line 48) | type TbaAlliance struct
type TbaScoreBreakdown (line 55) | type TbaScoreBreakdown struct
type tbaReef (line 90) | type tbaReef struct
type TbaRanking (line 100) | type TbaRanking struct
type TbaRankings (line 115) | type TbaRankings struct
type TbaTeam (line 120) | type TbaTeam struct
type TbaRobot (line 130) | type TbaRobot struct
type TbaAward (line 135) | type TbaAward struct
type TbaEvent (line 142) | type TbaEvent struct
type TbaMediaItem (line 146) | type TbaMediaItem struct
type TbaPublishedAward (line 151) | type TbaPublishedAward struct
function NewTbaClient (line 165) | func NewTbaClient(eventCode, secretId, secret string) *TbaClient {
function getTbaTeam (line 568) | func getTbaTeam(team int) string {
function createTbaAlliance (line 609) | func createTbaAlliance(teamIds [3]int, surrogates [3]bool, score *int, c...
function createTbaScoringBreakdown (line 630) | func createTbaScoringBreakdown(
FILE: partner/tba_test.go
function TestPublishTeams (line 20) | func TestPublishTeams(t *testing.T) {
function TestPublishMatches (line 46) | func TestPublishMatches(t *testing.T) {
function TestPublishRankings (line 92) | func TestPublishRankings(t *testing.T) {
function TestPublishAlliances (line 118) | func TestPublishAlliances(t *testing.T) {
function TestPublishingErrors (line 148) | func TestPublishingErrors(t *testing.T) {
function TestPublishAwards (line 171) | func TestPublishAwards(t *testing.T) {
function setupTestDb (line 200) | func setupTestDb(t *testing.T) *model.Database {
FILE: playoff/alliance_source.go
type allianceSource (line 10) | type allianceSource interface
type allianceSelectionSource (line 29) | type allianceSelectionSource struct
method AllianceId (line 33) | func (source allianceSelectionSource) AllianceId() int {
method displayName (line 37) | func (source allianceSelectionSource) displayName() string {
method setDestination (line 41) | func (source allianceSelectionSource) setDestination(destination Match...
method update (line 45) | func (source allianceSelectionSource) update(playoffMatchResults map[i...
method traverse (line 49) | func (source allianceSelectionSource) traverse(visitFunction func(Matc...
type matchupSource (line 55) | type matchupSource struct
method AllianceId (line 60) | func (source matchupSource) AllianceId() int {
method displayName (line 68) | func (source matchupSource) displayName() string {
method setDestination (line 75) | func (source matchupSource) setDestination(destination MatchGroup) {
method update (line 86) | func (source matchupSource) update(playoffMatchResults map[int]playoff...
method traverse (line 93) | func (source matchupSource) traverse(visitFunction func(MatchGroup) er...
FILE: playoff/break_spec.go
type breakSpec (line 8) | type breakSpec struct
FILE: playoff/double_elimination.go
function newDoubleEliminationBracket (line 15) | func newDoubleEliminationBracket(numAlliances int) (*Matchup, []breakSpe...
function newDoubleEliminationMatch (line 144) | func newDoubleEliminationMatch(number int, nameDetail string, durationSe...
FILE: playoff/double_elimination_test.go
function TestDoubleEliminationInitial (line 13) | func TestDoubleEliminationInitial(t *testing.T) {
function TestDoubleEliminationErrors (line 69) | func TestDoubleEliminationErrors(t *testing.T) {
function TestDoubleEliminationProgression (line 81) | func TestDoubleEliminationProgression(t *testing.T) {
FILE: playoff/match_group.go
type MatchGroup (line 14) | type MatchGroup interface
type matchSpec (line 29) | type matchSpec struct
function collectMatchGroups (line 44) | func collectMatchGroups(rootMatchGroup MatchGroup) (map[string]MatchGrou...
function collectMatchSpecs (line 59) | func collectMatchSpecs(rootMatchGroup MatchGroup) ([]*matchSpec, error) {
FILE: playoff/match_group_test.go
function TestCollectMatchGroupsErrors (line 12) | func TestCollectMatchGroupsErrors(t *testing.T) {
function TestCollectMatchSpecsErrors (line 35) | func TestCollectMatchSpecsErrors(t *testing.T) {
FILE: playoff/matchup.go
type Matchup (line 15) | type Matchup struct
method Id (line 30) | func (matchup *Matchup) Id() string {
method MatchSpecs (line 34) | func (matchup *Matchup) MatchSpecs() []*matchSpec {
method update (line 38) | func (matchup *Matchup) update(playoffMatchResults map[int]playoffMatc...
method setSourceDestinations (line 90) | func (matchup *Matchup) setSourceDestinations() {
method traverse (line 95) | func (matchup *Matchup) traverse(visitFunction func(MatchGroup) error)...
method RedAllianceSourceDisplayName (line 110) | func (matchup *Matchup) RedAllianceSourceDisplayName() string {
method BlueAllianceSourceDisplayName (line 116) | func (matchup *Matchup) BlueAllianceSourceDisplayName() string {
method RedAllianceDestination (line 121) | func (matchup *Matchup) RedAllianceDestination() string {
method BlueAllianceDestination (line 126) | func (matchup *Matchup) BlueAllianceDestination() string {
method StatusText (line 131) | func (matchup *Matchup) StatusText() (string, string) {
method WinningAllianceId (line 156) | func (matchup *Matchup) WinningAllianceId() int {
method LosingAllianceId (line 167) | func (matchup *Matchup) LosingAllianceId() int {
method IsComplete (line 178) | func (matchup *Matchup) IsComplete() bool {
method IsLosingAllianceEliminated (line 183) | func (matchup *Matchup) IsLosingAllianceEliminated() bool {
method isFinal (line 188) | func (matchup *Matchup) isFinal() bool {
method allianceDestination (line 193) | func (matchup *Matchup) allianceDestination(allianceId int) string {
function formatDestinationMatchName (line 217) | func formatDestinationMatchName(destination MatchGroup) string {
FILE: playoff/matchup_test.go
function TestMatchupAllianceSourceDisplayNames (line 12) | func TestMatchupAllianceSourceDisplayNames(t *testing.T) {
function TestMatchupStatusText (line 41) | func TestMatchupStatusText(t *testing.T) {
function TestMatchupHideUnnecessaryMatches (line 94) | func TestMatchupHideUnnecessaryMatches(t *testing.T) {
function TestMatchupOvertime (line 134) | func TestMatchupOvertime(t *testing.T) {
FILE: playoff/playoff_match_result.go
type playoffMatchResult (line 10) | type playoffMatchResult struct
FILE: playoff/playoff_tournament.go
type PlayoffTournament (line 15) | type PlayoffTournament struct
method MatchGroups (line 64) | func (tournament *PlayoffTournament) MatchGroups() map[string]MatchGro...
method FinalMatchup (line 69) | func (tournament *PlayoffTournament) FinalMatchup() *Matchup {
method IsComplete (line 74) | func (tournament *PlayoffTournament) IsComplete() bool {
method WinningAllianceId (line 80) | func (tournament *PlayoffTournament) WinningAllianceId() int {
method FinalistAllianceId (line 86) | func (tournament *PlayoffTournament) FinalistAllianceId() int {
method Traverse (line 91) | func (tournament *PlayoffTournament) Traverse(visitFunction func(Match...
method CreateMatchesAndBreaks (line 97) | func (tournament *PlayoffTournament) CreateMatchesAndBreaks(database *...
method UpdateMatches (line 186) | func (tournament *PlayoffTournament) UpdateMatches(database *model.Dat...
function NewPlayoffTournament (line 24) | func NewPlayoffTournament(playoffType model.PlayoffType, numPlayoffAllia...
function positionRedTeams (line 254) | func positionRedTeams(match *model.Match, alliance *model.Alliance) {
function positionBlueTeams (line 261) | func positionBlueTeams(match *model.Match, alliance *model.Alliance) {
FILE: playoff/playoff_tournament_test.go
function TestNewPlayoffTournamentErrors (line 15) | func TestNewPlayoffTournamentErrors(t *testing.T) {
function TestPlayoffTournamentGetters (line 22) | func TestPlayoffTournamentGetters(t *testing.T) {
function TestPlayoffTournamentCreateMatchesAndBreaks (line 40) | func TestPlayoffTournamentCreateMatchesAndBreaks(t *testing.T) {
function TestPlayoffTournamentUpdateMatches (line 132) | func TestPlayoffTournamentUpdateMatches(t *testing.T) {
FILE: playoff/single_elimination.go
function newSingleEliminationBracket (line 16) | func newSingleEliminationBracket(numAlliances int) (*Matchup, []breakSpe...
function newSingleEliminationAllianceSource (line 206) | func newSingleEliminationAllianceSource(matchup *Matchup, numAlliances i...
function newSingleEliminationMatch (line 220) | func newSingleEliminationMatch(longRoundName, shortRoundName string, set...
function newFinalMatches (line 232) | func newFinalMatches(startingOrder int) []*matchSpec {
FILE: playoff/single_elimination_test.go
function TestSingleEliminationInitialWith2Alliances (line 13) | func TestSingleEliminationInitialWith2Alliances(t *testing.T) {
function TestSingleEliminationInitialWith3Alliances (line 36) | func TestSingleEliminationInitialWith3Alliances(t *testing.T) {
function TestSingleEliminationInitialWith4Alliances (line 80) | func TestSingleEliminationInitialWith4Alliances(t *testing.T) {
function TestSingleEliminationInitialWith5Alliances (line 110) | func TestSingleEliminationInitialWith5Alliances(t *testing.T) {
function TestSingleEliminationInitialWith6Alliances (line 154) | func TestSingleEliminationInitialWith6Alliances(t *testing.T) {
function TestSingleEliminationInitialWith7Alliances (line 204) | func TestSingleEliminationInitialWith7Alliances(t *testing.T) {
function TestSingleEliminationInitialWith8Alliances (line 260) | func TestSingleEliminationInitialWith8Alliances(t *testing.T) {
function TestSingleEliminationInitialWith9Alliances (line 302) | func TestSingleEliminationInitialWith9Alliances(t *testing.T) {
function TestSingleEliminationInitialWith10Alliances (line 352) | func TestSingleEliminationInitialWith10Alliances(t *testing.T) {
function TestSingleEliminationInitialWith11Alliances (line 408) | func TestSingleEliminationInitialWith11Alliances(t *testing.T) {
function TestSingleEliminationInitialWith12Alliances (line 470) | func TestSingleEliminationInitialWith12Alliances(t *testing.T) {
function TestSingleEliminationInitialWith13Alliances (line 538) | func TestSingleEliminationInitialWith13Alliances(t *testing.T) {
function TestSingleEliminationInitialWith14Alliances (line 612) | func TestSingleEliminationInitialWith14Alliances(t *testing.T) {
function TestSingleEliminationInitialWith15Alliances (line 694) | func TestSingleEliminationInitialWith15Alliances(t *testing.T) {
function TestSingleEliminationInitialWith16Alliances (line 782) | func TestSingleEliminationInitialWith16Alliances(t *testing.T) {
function TestSingleEliminationErrors (line 872) | func TestSingleEliminationErrors(t *testing.T) {
function TestSingleEliminationProgression (line 884) | func TestSingleEliminationProgression(t *testing.T) {
function assertFullQuarterfinalsOnward (line 967) | func assertFullQuarterfinalsOnward(t *testing.T, matchSpecs []*matchSpec...
function assertFullSemifinalsOnward (line 991) | func assertFullSemifinalsOnward(t *testing.T, matchSpecs []*matchSpec, s...
function assertFullFinals (line 1009) | func assertFullFinals(t *testing.T, matchSpecs []*matchSpec, startingInd...
FILE: playoff/test_helpers.go
function setupTestDb (line 14) | func setupTestDb(t *testing.T) *model.Database {
type expectedMatchSpec (line 18) | type expectedMatchSpec struct
function assertMatchSpecs (line 31) | func assertMatchSpecs(
type expectedAlliances (line 54) | type expectedAlliances struct
function assertMatchSpecAlliances (line 59) | func assertMatchSpecAlliances(
function assertMatchGroups (line 72) | func assertMatchGroups(
function assertMatch (line 83) | func assertMatch(
function assertBreak (line 132) | func assertBreak(
function assertMatchupOutcome (line 147) | func assertMatchupOutcome(t *testing.T, matchGroup MatchGroup, redDestin...
FILE: plc/armorblock_string.go
function _ (line 7) | func _() {
constant _armorBlock_name (line 18) | _armorBlock_name = "redDsblueDsredIoLinkblueIoLinkarmorBlockCount"
method String (line 22) | func (i armorBlock) String() string {
FILE: plc/coil_string.go
function _ (line 7) | func _() {
constant _coil_name (line 28) | _coil_name = "heartbeatmatchResetstackLightGreenstackLightOrangestackLig...
method String (line 32) | func (i coil) String() string {
FILE: plc/fake_modbus_client_test.go
type FakeModbusClient (line 10) | type FakeModbusClient struct
method ReadCoils (line 17) | func (client *FakeModbusClient) ReadCoils(address, quantity uint16) (r...
method ReadDiscreteInputs (line 21) | func (client *FakeModbusClient) ReadDiscreteInputs(address, quantity u...
method WriteSingleCoil (line 32) | func (client *FakeModbusClient) WriteSingleCoil(address, value uint16)...
method WriteMultipleCoils (line 36) | func (client *FakeModbusClient) WriteMultipleCoils(address, quantity u...
method ReadInputRegisters (line 47) | func (client *FakeModbusClient) ReadInputRegisters(address, quantity u...
method ReadHoldingRegisters (line 51) | func (client *FakeModbusClient) ReadHoldingRegisters(address, quantity...
method WriteSingleRegister (line 64) | func (client *FakeModbusClient) WriteSingleRegister(address, value uin...
method WriteMultipleRegisters (line 68) | func (client *FakeModbusClient) WriteMultipleRegisters(
method ReadWriteMultipleRegisters (line 74) | func (client *FakeModbusClient) ReadWriteMultipleRegisters(
method MaskWriteRegister (line 80) | func (client *FakeModbusClient) MaskWriteRegister(address, andMask, or...
method ReadFIFOQueue (line 84) | func (client *FakeModbusClient) ReadFIFOQueue(address uint16) (results...
FILE: plc/input_string.go
function _ (line 7) | func _() {
constant _input_name (line 33) | _input_name = "fieldEStopred1EStopred1AStopred2EStopred2AStopred3EStopre...
method String (line 37) | func (i input) String() string {
FILE: plc/plc.go
type Plc (line 17) | type Plc interface
type ModbusPlc (line 40) | type ModbusPlc struct
method SetAddress (line 139) | func (plc *ModbusPlc) SetAddress(address string) {
method IsEnabled (line 150) | func (plc *ModbusPlc) IsEnabled() bool {
method IsHealthy (line 155) | func (plc *ModbusPlc) IsHealthy() bool {
method IoChangeNotifier (line 160) | func (plc *ModbusPlc) IoChangeNotifier() *websocket.Notifier {
method Run (line 165) | func (plc *ModbusPlc) Run() {
method GetArmorBlockStatuses (line 189) | func (plc *ModbusPlc) GetArmorBlockStatuses() map[string]bool {
method GetFieldEStop (line 198) | func (plc *ModbusPlc) GetFieldEStop() bool {
method GetTeamEStops (line 203) | func (plc *ModbusPlc) GetTeamEStops() ([3]bool, [3]bool) {
method GetTeamAStops (line 215) | func (plc *ModbusPlc) GetTeamAStops() ([3]bool, [3]bool) {
method GetEthernetConnected (line 227) | func (plc *ModbusPlc) GetEthernetConnected() ([3]bool, [3]bool) {
method ResetMatch (line 241) | func (plc *ModbusPlc) ResetMatch() {
method SetStackLights (line 253) | func (plc *ModbusPlc) SetStackLights(red, blue, orange, green bool) {
method SetStackBuzzer (line 261) | func (plc *ModbusPlc) SetStackBuzzer(state bool) {
method SetFieldResetLight (line 266) | func (plc *ModbusPlc) SetFieldResetLight(state bool) {
method GetCycleState (line 270) | func (plc *ModbusPlc) GetCycleState(max, index, duration int) bool {
method GetInputNames (line 274) | func (plc *ModbusPlc) GetInputNames() []string {
method GetRegisterNames (line 282) | func (plc *ModbusPlc) GetRegisterNames() []string {
method GetCoilNames (line 290) | func (plc *ModbusPlc) GetCoilNames() []string {
method GetProcessorCounts (line 299) | func (plc *ModbusPlc) GetProcessorCounts() (int, int) {
method SetTrussLights (line 305) | func (plc *ModbusPlc) SetTrussLights(redLights, blueLights [3]bool) {
method connect (line 314) | func (plc *ModbusPlc) connect() error {
method resetConnection (line 331) | func (plc *ModbusPlc) resetConnection() {
method update (line 339) | func (plc *ModbusPlc) update() {
method readInputs (line 365) | func (plc *ModbusPlc) readInputs() bool {
method readRegisters (line 384) | func (plc *ModbusPlc) readRegisters() bool {
method writeCoils (line 407) | func (plc *ModbusPlc) writeCoils() bool {
method generateIoChangeMessage (line 426) | func (plc *ModbusPlc) generateIoChangeMessage() any {
constant modbusPort (line 57) | modbusPort = 502
constant plcLoopPeriodMs (line 58) | plcLoopPeriodMs = 100
constant plcRetryIntevalSec (line 59) | plcRetryIntevalSec = 3
constant cycleCounterMax (line 60) | cycleCounterMax = 100
type input (line 66) | type input
constant fieldEStop (line 69) | fieldEStop input = iota
constant red1EStop (line 70) | red1EStop
constant red1AStop (line 71) | red1AStop
constant red2EStop (line 72) | red2EStop
constant red2AStop (line 73) | red2AStop
constant red3EStop (line 74) | red3EStop
constant red3AStop (line 75) | red3AStop
constant blue1EStop (line 76) | blue1EStop
constant blue1AStop (line 77) | blue1AStop
constant blue2EStop (line 78) | blue2EStop
constant blue2AStop (line 79) | blue2AStop
constant blue3EStop (line 80) | blue3EStop
constant blue3AStop (line 81) | blue3AStop
constant redConnected1 (line 82) | redConnected1
constant redConnected2 (line 83) | redConnected2
constant redConnected3 (line 84) | redConnected3
constant blueConnected1 (line 85) | blueConnected1
constant blueConnected2 (line 86) | blueConnected2
constant blueConnected3 (line 87) | blueConnected3
constant inputCount (line 88) | inputCount
type register (line 94) | type register
constant fieldIoConnection (line 97) | fieldIoConnection register = iota
constant redProcessor (line 98) | redProcessor
constant blueProcessor (line 99) | blueProcessor
constant registerCount (line 100) | registerCount
type coil (line 106) | type coil
constant heartbeat (line 109) | heartbeat coil = iota
constant matchReset (line 110) | matchReset
constant stackLightGreen (line 111) | stackLightGreen
constant stackLightOrange (line 112) | stackLightOrange
constant stackLightRed (line 113) | stackLightRed
constant stackLightBlue (line 114) | stackLightBlue
constant stackLightBuzzer (line 115) | stackLightBuzzer
constant fieldResetLight (line 116) | fieldResetLight
constant redTrussLightOuter (line 117) | redTrussLightOuter
constant redTrussLightMiddle (line 118) | redTrussLightMiddle
constant redTrussLightInner (line 119) | redTrussLightInner
constant blueTrussLightOuter (line 120) | blueTrussLightOuter
constant blueTrussLightMiddle (line 121) | blueTrussLightMiddle
constant blueTrussLightInner (line 122) | blueTrussLightInner
constant coilCount (line 123) | coilCount
type armorBlock (line 129) | type armorBlock
constant redDs (line 132) | redDs armorBlock = iota
constant blueDs (line 133) | blueDs
constant redIoLink (line 134) | redIoLink
constant blueIoLink (line 135) | blueIoLink
constant armorBlockCount (line 136) | armorBlockCount
function byteToBool (line 434) | func byteToBool(bytes []byte, size int) []bool {
function byteToUint (line 445) | func byteToUint(bytes []byte, size int) []uint16 {
function boolToByte (line 453) | func boolToByte(bools []bool) []byte {
FILE: plc/plc_test.go
function TestPlcInitialization (line 13) | func TestPlcInitialization(t *testing.T) {
function TestPlcGetCycleState (line 27) | func TestPlcGetCycleState(t *testing.T) {
function TestPlcGetNames (line 60) | func TestPlcGetNames(t *testing.T) {
function TestPlcInputs (line 121) | func TestPlcInputs(t *testing.T) {
function TestPlcInputsGameSpecific (line 293) | func TestPlcInputsGameSpecific(t *testing.T) {
function TestPlcRegisters (line 303) | func TestPlcRegisters(t *testing.T) {
function TestPlcRegistersGameSpecific (line 340) | func TestPlcRegistersGameSpecific(t *testing.T) {
function TestPlcCoils (line 365) | func TestPlcCoils(t *testing.T) {
function TestPlcCoilsGameSpecific (line 434) | func TestPlcCoilsGameSpecific(t *testing.T) {
function TestPlcIsHealthy (line 464) | func TestPlcIsHealthy(t *testing.T) {
function TestByteToBool (line 486) | func TestByteToBool(t *testing.T) {
function TestByteToUint (line 497) | func TestByteToUint(t *testing.T) {
function TestBoolToByte (line 505) | func TestBoolToByte(t *testing.T) {
FILE: plc/register_string.go
function _ (line 7) | func _() {
constant _register_name (line 17) | _register_name = "fieldIoConnectionredProcessorblueProcessorregisterCount"
method String (line 21) | func (i register) String() string {
FILE: static/js/lib/handlebars-1.3.0.js
function SafeString (line 34) | function SafeString(string) {
function escapeChar (line 65) | function escapeChar(chr) {
function extend (line 69) | function extend(obj, value) {
function escapeExpression (line 97) | function escapeExpression(string) {
function isEmpty (line 114) | function isEmpty(value) {
function Exception (line 135) | function Exception(message, node) {
function HandlebarsEnvironment (line 184) | function HandlebarsEnvironment(helpers, partials) {
function registerDefaultHelpers (line 216) | function registerDefaultHelpers(instance) {
function log (line 337) | function log(level, obj) { logger.log(level, obj); }
function checkRevision (line 357) | function checkRevision(compilerInfo) {
function template (line 377) | function template(templateSpec, env) {
function programWithDepth (line 451) | function programWithDepth(i, fn, data /*, $depth */) {
function program (line 464) | function program(i, fn, data) {
function invokePartial (line 475) | function invokePartial(partial, name, context, helpers, partials, data) {
function noop (line 485) | function noop() { return ""; }
function LocationInfo (line 535) | function LocationInfo(locInfo){
function popStack (line 886) | function popStack(n) {
function lex (line 891) | function lex() {
function stripFlags (line 979) | function stripFlags(open, close) {
function strip (line 1158) | function strip(start, end) {
function Parser (line 1251) | function Parser () { this.yy = {}; }
function parse (line 1267) | function parse(input) {
function Compiler (line 1285) | function Compiler() {}
function precompile (line 1706) | function precompile(input, options, env) {
function compile (line 1721) | function compile(input, options, env) {
function Literal (line 1763) | function Literal(value) {
function JavaScriptCompiler (line 1767) | function JavaScriptCompiler() {}
FILE: tournament/awards.go
function CreateOrUpdateAward (line 14) | func CreateOrUpdateAward(database *model.Database, award *model.Award, c...
function DeleteAward (line 73) | func DeleteAward(database *model.Database, awardId int) error {
function CreateOrUpdateWinnerAndFinalistAwards (line 93) | func CreateOrUpdateWinnerAndFinalistAwards(database *model.Database, win...
function createOrUpdateAwardLowerThird (line 161) | func createOrUpdateAwardLowerThird(
FILE: tournament/awards_test.go
function TestCreateOrUpdateAwardWithIntro (line 12) | func TestCreateOrUpdateAwardWithIntro(t *testing.T) {
function TestCreateOrUpdateAwardWithoutIntro (line 51) | func TestCreateOrUpdateAwardWithoutIntro(t *testing.T) {
function TestCreateOrUpdateWinnerAndFinalistAwards (line 91) | func TestCreateOrUpdateWinnerAndFinalistAwards(t *testing.T) {
FILE: tournament/judging_schedule.go
type JudgingScheduleParams (line 16) | type JudgingScheduleParams struct
type judgeSchedule (line 34) | type judgeSchedule struct
function BuildJudgingSchedule (line 41) | func BuildJudgingSchedule(database *model.Database, params JudgingSchedu...
function createTeamMatchMap (line 199) | func createTeamMatchMap(teams []model.Team, matches []model.Match) map[i...
function getNextSlotForTeam (line 218) | func getNextSlotForTeam(
FILE: tournament/judging_schedule_test.go
function TestBuildJudgingSchedule (line 14) | func TestBuildJudgingSchedule(t *testing.T) {
function TestBuildJudgingScheduleMissingTeamMatches (line 110) | func TestBuildJudgingScheduleMissingTeamMatches(t *testing.T) {
function TestGetNextSlotForTeamAfterLastMatch (line 158) | func TestGetNextSlotForTeamAfterLastMatch(t *testing.T) {
function TestBuildJudgingScheduleAllowsEndDuringBreak (line 187) | func TestBuildJudgingScheduleAllowsEndDuringBreak(t *testing.T) {
FILE: tournament/qualification_rankings.go
function CalculateRankings (line 17) | func CalculateRankings(database *model.Database, preservePreviousRank bo...
function CalculateTeamCards (line 81) | func CalculateTeamCards(database *model.Database, matchType model.MatchT...
function addMatchResultToRankings (line 135) | func addMatchResultToRankings(
function sortRankings (line 163) | func sortRankings(rankings map[int]*game.Ranking) game.Rankings {
FILE: tournament/qualification_rankings_test.go
function TestCalculateRankings (line 15) | func TestCalculateRankings(t *testing.T) {
function TestAddMatchResultToRankingsHandleCards (line 107) | func TestAddMatchResultToRankingsHandleCards(t *testing.T) {
function setupMatchResultsForRankings (line 127) | func setupMatchResultsForRankings(database *model.Database) {
FILE: tournament/schedule.go
constant schedulesDir (line 21) | schedulesDir = "schedules"
constant TeamsPerMatch (line 22) | TeamsPerMatch = 6
function BuildRandomSchedule (line 26) | func BuildRandomSchedule(
function countMatches (line 109) | func countMatches(scheduleBlocks []model.ScheduleBlock) int {
FILE: tournament/schedule_test.go
function TestMain (line 17) | func TestMain(m *testing.M) {
function TestNonExistentSchedule (line 21) | func TestNonExistentSchedule(t *testing.T) {
function TestMalformedSchedule (line 31) | func TestMalformedSchedule(t *testing.T) {
function TestScheduleTeams (line 55) | func TestScheduleTeams(t *testing.T) {
function TestScheduleTiming (line 91) | func TestScheduleTiming(t *testing.T) {
function TestScheduleSurrogates (line 108) | func TestScheduleSurrogates(t *testing.T) {
function assertMatch (line 133) | func assertMatch(
FILE: tournament/test_helpers.go
function CreateTestAlliances (line 13) | func CreateTestAlliances(database *model.Database, allianceCount int) {
function setupTestDb (line 24) | func setupTestDb(t *testing.T) *model.Database {
FILE: web/alliance_selection.go
constant allianceSelectionBreakDurationSec (line 27) | allianceSelectionBreakDurationSec = 120
method allianceSelectionGetHandler (line 33) | func (web *Web) allianceSelectionGetHandler(w http.ResponseWriter, r *ht...
method allianceSelectionPostHandler (line 42) | func (web *Web) allianceSelectionPostHandler(w http.ResponseWriter, r *h...
method allianceSelectionStartHandler (line 103) | func (web *Web) allianceSelectionStartHandler(w http.ResponseWriter, r *...
method allianceSelectionResetHandler (line 148) | func (web *Web) allianceSelectionResetHandler(w http.ResponseWriter, r *...
method allianceSelectionFinalizeHandler (line 178) | func (web *Web) allianceSelectionFinalizeHandler(w http.ResponseWriter, ...
method allianceSelectionWebsocketHandler (line 267) | func (web *Web) allianceSelectionWebsocketHandler(w http.ResponseWriter,...
method renderAllianceSelection (line 361) | func (web *Web) renderAllianceSelection(w http.ResponseWriter, r *http.R...
method canModifyAllianceSelection (line 406) | func (web *Web) canModifyAllianceSelection() bool {
method canResetAllianceSelection (line 415) | func (web *Web) canResetAllianceSelection() bool {
method determineNextCell (line 429) | func (web *Web) determineNextCell() (int, int) {
FILE: web/alliance_selection_test.go
function TestAllianceSelection (line 17) | func TestAllianceSelection(t *testing.T) {
function TestAllianceSelectionErrors (line 106) | func TestAllianceSelectionErrors(t *testing.T) {
function TestAllianceSelectionReset (line 169) | func TestAllianceSelectionReset(t *testing.T) {
function TestAllianceSelectionAutofocus (line 228) | func TestAllianceSelectionAutofocus(t *testing.T) {
function TestAllianceSelectionWebsocket (line 319) | func TestAllianceSelectionWebsocket(t *testing.T) {
FILE: web/alliance_station_display.go
method allianceStationDisplayHandler (line 15) | func (web *Web) allianceStationDisplayHandler(w http.ResponseWriter, r *...
method allianceStationDisplayWebsocketHandler (line 37) | func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWr...
FILE: web/alliance_station_display_test.go
function TestAllianceStationDisplay (line 15) | func TestAllianceStationDisplay(t *testing.T) {
function TestAllianceStationDisplayWebsocket (line 28) | func TestAllianceStationDisplayWebsocket(t *testing.T) {
FILE: web/announcer_display.go
method announcerDisplayHandler (line 15) | func (web *Web) announcerDisplayHandler(w http.ResponseWriter, r *http.R...
method announcerDisplayMatchLoadHandler (line 37) | func (web *Web) announcerDisplayMatchLoadHandler(w http.ResponseWriter, ...
method announcerDisplayScorePostedHandler (line 52) | func (web *Web) announcerDisplayScorePostedHandler(w http.ResponseWriter...
method announcerDisplayWebsocketHandler (line 67) | func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, ...
FILE: web/announcer_display_test.go
function TestAnnouncerDisplay (line 14) | func TestAnnouncerDisplay(t *testing.T) {
function TestAnnouncerDisplayMatchLoad (line 22) | func TestAnnouncerDisplayMatchLoad(t *testing.T) {
function TestAnnouncerDisplayScorePosted (line 34) | func TestAnnouncerDisplayScorePosted(t *testing.T) {
function TestAnnouncerDisplayWebsocket (line 44) | func TestAnnouncerDisplayWebsocket(t *testing.T) {
FILE: web/api.go
type MatchResultWithSummary (line 22) | type MatchResultWithSummary struct
type MatchWithResult (line 28) | type MatchWithResult struct
type RankingWithNickname (line 33) | type RankingWithNickname struct
type allianceMatchup (line 38) | type allianceMatchup struct
method matchesApiHandler (line 51) | func (web *Web) matchesApiHandler(w http.ResponseWriter, r *http.Request) {
method sponsorSlidesApiHandler (line 97) | func (web *Web) sponsorSlidesApiHandler(w http.ResponseWriter, r *http.R...
method rankingsApiHandler (line 124) | func (web *Web) rankingsApiHandler(w http.ResponseWriter, r *http.Reques...
method alliancesApiHandler (line 185) | func (web *Web) alliancesApiHandler(w http.ResponseWriter, r *http.Reque...
method arenaWebsocketApiHandler (line 208) | func (web *Web) arenaWebsocketApiHandler(w http.ResponseWriter, r *http....
method teamAvatarsApiHandler (line 221) | func (web *Web) teamAvatarsApiHandler(w http.ResponseWriter, r *http.Req...
method bracketSvgApiHandler (line 236) | func (web *Web) bracketSvgApiHandler(w http.ResponseWriter, r *http.Requ...
method generateBracketSvg (line 254) | func (web *Web) generateBracketSvg(w io.Writer, activeMatch *model.Match...
FILE: web/api_test.go
function TestMatchesApi (line 18) | func TestMatchesApi(t *testing.T) {
function TestRankingsApi (line 80) | func TestRankingsApi(t *testing.T) {
function TestSponsorSlidesApi (line 118) | func TestSponsorSlidesApi(t *testing.T) {
function TestAlliancesApi (line 138) | func TestAlliancesApi(t *testing.T) {
function TestArenaWebsocketApi (line 165) | func TestArenaWebsocketApi(t *testing.T) {
function TestBracketSvgApiDoubleElimination (line 181) | func TestBracketSvgApiDoubleElimination(t *testing.T) {
FILE: web/audience_display.go
method audienceDisplayHandler (line 16) | func (web *Web) audienceDisplayHandler(w http.ResponseWriter, r *http.Re...
method audienceDisplayWebsocketHandler (line 46) | func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r...
FILE: web/audience_display_test.go
function TestAudienceDisplay (line 13) | func TestAudienceDisplay(t *testing.T) {
function TestAudienceDisplayWebsocket (line 30) | func TestAudienceDisplayWebsocket(t *testing.T) {
FILE: web/bracket_display.go
method bracketDisplayHandler (line 15) | func (web *Web) bracketDisplayHandler(w http.ResponseWriter, r *http.Req...
method bracketDisplayWebsocketHandler (line 36) | func (web *Web) bracketDisplayWebsocketHandler(w http.ResponseWriter, r ...
FILE: web/bracket_display_test.go
function TestBracketDisplay (line 13) | func TestBracketDisplay(t *testing.T) {
function TestBracketDisplayWebsocket (line 21) | func TestBracketDisplayWebsocket(t *testing.T) {
FILE: web/display_utils.go
method enforceDisplayConfiguration (line 18) | func (web *Web) enforceDisplayConfiguration(w http.ResponseWriter, r *ht...
method registerDisplay (line 58) | func (web *Web) registerDisplay(r *http.Request) (*field.Display, error) {
FILE: web/field_monitor_display.go
method fieldMonitorDisplayHandler (line 18) | func (web *Web) fieldMonitorDisplayHandler(w http.ResponseWriter, r *htt...
method fieldMonitorDisplayWebsocketHandler (line 43) | func (web *Web) fieldMonitorDisplayWebsocketHandler(w http.ResponseWrite...
FILE: web/field_monitor_display_test.go
function TestFieldMonitorDisplay (line 15) | func TestFieldMonitorDisplay(t *testing.T) {
function TestFieldMonitorDisplayWebsocket (line 23) | func TestFieldMonitorDisplayWebsocket(t *testing.T) {
function TestFieldMonitorFtaDisplayWebsocket (line 52) | func TestFieldMonitorFtaDisplayWebsocket(t *testing.T) {
FILE: web/login.go
method loginHandler (line 18) | func (web *Web) loginHandler(w http.ResponseWriter, r *http.Request) {
method loginPostHandler (line 23) | func (web *Web) loginPostHandler(w http.ResponseWriter, r *http.Request) {
method renderLogin (line 44) | func (web *Web) renderLogin(w http.ResponseWriter, r *http.Request, erro...
method userIsAdmin (line 62) | func (web *Web) userIsAdmin(w http.ResponseWriter, r *http.Request) bool {
method getUserSessionFromCookie (line 80) | func (web *Web) getUserSessionFromCookie(r *http.Request) *model.UserSes...
method checkAuthPassword (line 89) | func (web *Web) checkAuthPassword(user, password string) error {
FILE: web/login_test.go
function TestLoginDisplay (line 11) | func TestLoginDisplay(t *testing.T) {
FILE: web/logo_display.go
method logoDisplayHandler (line 15) | func (web *Web) logoDisplayHandler(w http.ResponseWriter, r *http.Reques...
method logoDisplayWebsocketHandler (line 36) | func (web *Web) logoDisplayWebsocketHandler(w http.ResponseWriter, r *ht...
FILE: web/logo_display_test.go
function TestLogoDisplay (line 13) | func TestLogoDisplay(t *testing.T) {
function TestLogoDisplayWebsocket (line 21) | func TestLogoDisplayWebsocket(t *testing.T) {
FILE: web/match_logs.go
type MatchLogsListItem (line 20) | type MatchLogsListItem struct
type MatchLogRow (line 30) | type MatchLogRow struct
type MatchLog (line 51) | type MatchLog struct
type MatchLogs (line 57) | type MatchLogs struct
method matchLogsHandler (line 64) | func (web *Web) matchLogsHandler(w http.ResponseWriter, r *http.Request) {
method matchLogsViewGetHandler (line 108) | func (web *Web) matchLogsViewGetHandler(w http.ResponseWriter, r *http.R...
method getMatchLogFromRequest (line 138) | func (web *Web) getMatchLogFromRequest(r *http.Request) (*model.Match, *...
method buildMatchLogsList (line 240) | func (web *Web) buildMatchLogsList(matchType model.MatchType) ([]MatchLo...
FILE: web/match_play.go
type MatchPlayListItem (line 24) | type MatchPlayListItem struct
type MatchPlayList (line 32) | type MatchPlayList
method Len (line 519) | func (list MatchPlayList) Len() int {
method Less (line 524) | func (list MatchPlayList) Less(i, j int) bool {
method Swap (line 529) | func (list MatchPlayList) Swap(i, j int) {
method matchPlayHandler (line 35) | func (web *Web) matchPlayHandler(w http.ResponseWriter, r *http.Request) {
method matchPlayMatchLoadHandler (line 64) | func (web *Web) matchPlayMatchLoadHandler(w http.ResponseWriter, r *http...
method matchPlayWebsocketHandler (line 115) | func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http...
method commitMatchScore (line 378) | func (web *Web) commitMatchScore(match *model.Match, matchResult *model....
method getCurrentMatchResult (line 502) | func (web *Web) getCurrentMatchResult() *model.MatchResult {
method commitCurrentMatchScore (line 514) | func (web *Web) commitCurrentMatchScore() error {
method buildMatchPlayList (line 534) | func (web *Web) buildMatchPlayList(matchType model.MatchType) (MatchPlay...
FILE: web/match_play_test.go
function TestMatchPlay (line 21) | func TestMatchPlay(t *testing.T) {
function TestMatchPlayMatchList (line 30) | func TestMatchPlayMatchList(t *testing.T) {
function TestCommitMatch (line 54) | func TestCommitMatch(t *testing.T) {
function TestCommitTiebreak (line 110) | func TestCommitTiebreak(t *testing.T) {
function TestCommitCards (line 189) | func TestCommitCards(t *testing.T) {
function TestMatchPlayWebsocketCommands (line 269) | func TestMatchPlayWebsocketCommands(t *testing.T) {
function TestMatchPlayWebsocketLoadMatch (line 357) | func TestMatchPlayWebsocketLoadMatch(t *testing.T) {
function TestMatchPlayWebsocketShowAndClearResult (line 420) | func TestMatchPlayWebsocketShowAndClearResult(t *testing.T) {
function TestMatchPlayWebsocketNotifications (line 455) | func TestMatchPlayWebsocketNotifications(t *testing.T) {
function readWebsocketStatusMatchTime (line 525) | func readWebsocketStatusMatchTime(t *testing.T, ws *websocket.Websocket)...
function getStatusMatchTime (line 529) | func getStatusMatchTime(t *testing.T, messages map[string]any) (bool, fi...
FILE: web/match_review.go
type MatchReviewListItem (line 17) | type MatchReviewListItem struct
method matchReviewHandler (line 30) | func (web *Web) matchReviewHandler(w http.ResponseWriter, r *http.Reques...
method matchReviewEditGetHandler (line 74) | func (web *Web) matchReviewEditGetHandler(w http.ResponseWriter, r *http...
method matchReviewEditPostHandler (line 110) | func (web *Web) matchReviewEditPostHandler(w http.ResponseWriter, r *htt...
method getMatchResultFromRequest (line 153) | func (web *Web) getMatchResultFromRequest(r *http.Request) (*model.Match...
method buildMatchReviewList (line 182) | func (web *Web) buildMatchReviewList(matchType model.MatchType) ([]Match...
FILE: web/match_review_test.go
function TestMatchReview (line 16) | func TestMatchReview(t *testing.T) {
function TestMatchReviewEditExistingResult (line 40) | func TestMatchReviewEditExistingResult(t *testing.T) {
function TestMatchReviewCreateNewResult (line 89) | func TestMatchReviewCreateNewResult(t *testing.T) {
function TestMatchReviewEditCurrentMatch (line 130) | func TestMatchReviewEditCurrentMatch(t *testing.T) {
FILE: web/placeholder_display.go
method placeholderDisplayHandler (line 15) | func (web *Web) placeholderDisplayHandler(w http.ResponseWriter, r *http...
method placeholderDisplayWebsocketHandler (line 36) | func (web *Web) placeholderDisplayWebsocketHandler(w http.ResponseWriter...
FILE: web/placeholder_display_test.go
function TestPlaceholderDisplay (line 14) | func TestPlaceholderDisplay(t *testing.T) {
function TestPlaceholderDisplayWebsocket (line 26) | func TestPlaceholderDisplayWebsocket(t *testing.T) {
FILE: web/queueing_display.go
constant numNonPlayoffMatchesToShow (line 17) | numNonPlayoffMatchesToShow = 5
constant numPlayoffMatchesToShow (line 18) | numPlayoffMatchesToShow = 4
method queueingDisplayHandler (line 22) | func (web *Web) queueingDisplayHandler(w http.ResponseWriter, r *http.Re...
method queueingDisplayMatchLoadHandler (line 46) | func (web *Web) queueingDisplayMatchLoadHandler(w http.ResponseWriter, r...
method queueingDisplayWebsocketHandler (line 110) | func (web *Web) queueingDisplayWebsocketHandler(w http.ResponseWriter, r...
FILE: web/queueing_display_test.go
function TestQueueingDisplay (line 13) | func TestQueueingDisplay(t *testing.T) {
function TestQueueingDisplayWebsocket (line 21) | func TestQueueingDisplayWebsocket(t *testing.T) {
FILE: web/rankings_display.go
method rankingsDisplayHandler (line 15) | func (web *Web) rankingsDisplayHandler(w http.ResponseWriter, r *http.Re...
method rankingsDisplayWebsocketHandler (line 36) | func (web *Web) rankingsDisplayWebsocketHandler(w http.ResponseWriter, r...
FILE: web/rankings_display_test.go
function TestRankingsDisplay (line 13) | func TestRankingsDisplay(t *testing.T) {
function TestRankingsDisplayWebsocket (line 21) | func TestRankingsDisplayWebsocket(t *testing.T) {
FILE: web/referee_panel.go
method refereePanelHandler (line 22) | func (web *Web) refereePanelHandler(w http.ResponseWriter, r *http.Reque...
method refereePanelFoulListHandler (line 44) | func (web *Web) refereePanelFoulListHandler(w http.ResponseWriter, r *ht...
method refereePanelWebsocketHandler (line 70) | func (web *Web) refereePanelWebsocketHandler(w http.ResponseWriter, r *h...
FILE: web/referee_panel_test.go
function TestRefereePanel (line 16) | func TestRefereePanel(t *testing.T) {
function TestRefereePanelWebsocket (line 24) | func TestRefereePanelWebsocket(t *testing.T) {
FILE: web/reports.go
method rankingsCsvReportHandler (line 25) | func (web *Web) rankingsCsvReportHandler(w http.ResponseWriter, r *http....
method rankingsPdfReportHandler (line 52) | func (web *Web) rankingsPdfReportHandler(w http.ResponseWriter, r *http....
method findBackupTeams (line 128) | func (web *Web) findBackupTeams(rankings game.Rankings) (game.Rankings, ...
type backupTeam (line 165) | type backupTeam struct
method backupTeamsCsvReportHandler (line 173) | func (web *Web) backupTeamsCsvReportHandler(w http.ResponseWriter, r *ht...
method backupsPdfReportHandler (line 220) | func (web *Web) backupsPdfReportHandler(w http.ResponseWriter, r *http.R...
constant cHPad (line 277) | cHPad = 5
constant cVPad (line 278) | cVPad = 5
constant cWidth (line 279) | cWidth = 95
constant cHeight (line 280) | cHeight = 60
constant cSideMargin (line 281) | cSideMargin = 10
constant cTopMargin (line 282) | cTopMargin = 10
constant cImgWidth (line 283) | cImgWidth = 25
constant cWOffset (line 284) | cWOffset = 5
method couponsPdfReportHandler (line 287) | func (web *Web) couponsPdfReportHandler(w http.ResponseWriter, r *http.R...
function drawCoupon (line 321) | func drawCoupon(pdf gofpdf.Pdf, eventName string, x float64, y float64, ...
function drawEventWatermark (line 333) | func drawEventWatermark(pdf gofpdf.Pdf, x float64, y float64, name strin...
function drawCenteredText (line 351) | func drawCenteredText(pdf gofpdf.Pdf, txt string, x float64, y float64) {
function drawPdfLogo (line 356) | func drawPdfLogo(pdf gofpdf.Pdf, x float64, y float64, width float64) {
method scheduleCsvReportHandler (line 371) | func (web *Web) scheduleCsvReportHandler(w http.ResponseWriter, r *http....
method schedulePdfReportHandler (line 404) | func (web *Web) schedulePdfReportHandler(w http.ResponseWriter, r *http....
method teamsCsvReportHandler (line 548) | func (web *Web) teamsCsvReportHandler(w http.ResponseWriter, r *http.Req...
method teamsPdfReportHandler (line 575) | func (web *Web) teamsPdfReportHandler(w http.ResponseWriter, r *http.Req...
method wpaKeysCsvReportHandler (line 652) | func (web *Web) wpaKeysCsvReportHandler(w http.ResponseWriter, r *http.R...
method alliancesPdfReportHandler (line 675) | func (web *Web) alliancesPdfReportHandler(w http.ResponseWriter, r *http...
method bracketPdfReportHandler (line 797) | func (web *Web) bracketPdfReportHandler(w http.ResponseWriter, r *http.R...
function surrogateText (line 823) | func surrogateText(isSurrogate bool) string {
method cyclePdfReportHandler (line 832) | func (web *Web) cyclePdfReportHandler(w http.ResponseWriter, r *http.Req...
method ftaCsvReportHandler (line 936) | func (web *Web) ftaCsvReportHandler(w http.ResponseWriter, r *http.Reque...
method judgingSchedulePdfReportHandler (line 963) | func (web *Web) judgingSchedulePdfReportHandler(w http.ResponseWriter, r...
function addTimeGeneratedFooter (line 1086) | func addTimeGeneratedFooter(pdf *gofpdf.Fpdf) {
function drawMultiLineCell (line 1095) | func drawMultiLineCell(pdf *gofpdf.Fpdf, width, height, lineHeight float...
FILE: web/reports_test.go
function TestRankingsCsvReport (line 15) | func TestRankingsCsvReport(t *testing.T) {
function TestRankingsPdfReport (line 31) | func TestRankingsPdfReport(t *testing.T) {
function TestScheduleCsvReport (line 45) | func TestScheduleCsvReport(t *testing.T) {
function TestSchedulePdfReport (line 103) | func TestSchedulePdfReport(t *testing.T) {
function TestTeamsCsvReport (line 133) | func TestTeamsCsvReport(t *testing.T) {
function TestTeamsPdfReport (line 170) | func TestTeamsPdfReport(t *testing.T) {
function TestWpaKeysCsvReport (line 191) | func TestWpaKeysCsvReport(t *testing.T) {
function TestAlliancesPdfReport (line 206) | func TestAlliancesPdfReport(t *testing.T) {
function TestBracketPdfReport (line 217) | func TestBracketPdfReport(t *testing.T) {
function TestJudgingSchedulePdfReport (line 226) | func TestJudgingSchedulePdfReport(t *testing.T) {
FILE: web/scoring_panel.go
type ScoringPosition (line 21) | type ScoringPosition struct
method scoringPanelHandler (line 76) | func (web *Web) scoringPanelHandler(w http.ResponseWriter, r *http.Reque...
method scoringPanelWebsocketHandler (line 107) | func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *h...
FILE: web/scoring_panel_test.go
function TestScoringPanel (line 16) | func TestScoringPanel(t *testing.T) {
function TestScoringPanelWebsocket (line 33) | func TestScoringPanelWebsocket(t *testing.T) {
FILE: web/setup_awards.go
method awardsGetHandler (line 16) | func (web *Web) awardsGetHandler(w http.ResponseWriter, r *http.Request) {
method awardsPostHandler (line 53) | func (web *Web) awardsPostHandler(w http.ResponseWriter, r *http.Request) {
FILE: web/setup_awards_test.go
function TestSetupAwards (line 12) | func TestSetupAwards(t *testing.T) {
FILE: web/setup_breaks.go
method breaksGetHandler (line 15) | func (web *Web) breaksGetHandler(w http.ResponseWriter, r *http.Request) {
method breaksPostHandler (line 42) | func (web *Web) breaksPostHandler(w http.ResponseWriter, r *http.Request) {
FILE: web/setup_breaks_test.go
function TestSetupBreaks (line 13) | func TestSetupBreaks(t *testing.T) {
FILE: web/setup_displays.go
method displaysGetHandler (line 20) | func (web *Web) displaysGetHandler(w http.ResponseWriter, r *http.Reques...
method displaysWebsocketHandler (line 42) | func (web *Web) displaysWebsocketHandler(w http.ResponseWriter, r *http....
FILE: web/setup_displays_test.go
function TestSetupDisplays (line 15) | func TestSetupDisplays(t *testing.T) {
function TestSetupDisplaysWebsocket (line 23) | func TestSetupDisplaysWebsocket(t *testing.T) {
function TestSetupDisplaysWebsocketReloadDisplays (line 82) | func TestSetupDisplaysWebsocketReloadDisplays(t *testing.T) {
function readDisplayConfiguration (line 109) | func readDisplayConfiguration(t *testing.T, ws *websocket.Websocket) map...
FILE: web/setup_field_testing.go
method fieldTestingGetHandler (line 19) | func (web *Web) fieldTestingGetHandler(w http.ResponseWriter, r *http.Re...
method fieldTestingWebsocketHandler (line 45) | func (web *Web) fieldTestingWebsocketHandler(w http.ResponseWriter, r *h...
FILE: web/setup_field_testing_test.go
function TestSetupFieldTesting (line 14) | func TestSetupFieldTesting(t *testing.T) {
function TestSetupFieldTestingWebsocket (line 24) | func TestSetupFieldTestingWebsocket(t *testing.T) {
FILE: web/setup_judging.go
method judgingGetHandler (line 25) | func (web *Web) judgingGetHandler(w http.ResponseWriter, r *http.Request) {
method judgingGeneratePostHandler (line 34) | func (web *Web) judgingGeneratePostHandler(w http.ResponseWriter, r *htt...
method judgingClearPostHandler (line 92) | func (web *Web) judgingClearPostHandler(w http.ResponseWriter, r *http.R...
method renderJudging (line 106) | func (web *Web) renderJudging(w http.ResponseWriter, r *http.Request, er...
FILE: web/setup_judging_test.go
function TestSetupJudging (line 14) | func TestSetupJudging(t *testing.T) {
FILE: web/setup_lower_thirds.go
method lowerThirdsGetHandler (line 19) | func (web *Web) lowerThirdsGetHandler(w http.ResponseWriter, r *http.Req...
method lowerThirdsWebsocketHandler (line 48) | func (web *Web) lowerThirdsWebsocketHandler(w http.ResponseWriter, r *ht...
method saveLowerThird (line 158) | func (web *Web) saveLowerThird(lowerThird *model.LowerThird) error {
method reorderLowerThird (line 180) | func (web *Web) reorderLowerThird(id int, moveUp bool) error {
FILE: web/setup_lower_thirds_test.go
function TestSetupLowerThirds (line 15) | func TestSetupLowerThirds(t *testing.T) {
FILE: web/setup_schedule.go
method scheduleGetHandler (line 22) | func (web *Web) scheduleGetHandler(w http.ResponseWriter, r *http.Reques...
method scheduleGeneratePostHandler (line 38) | func (web *Web) scheduleGeneratePostHandler(w http.ResponseWriter, r *ht...
method scheduleSavePostHandler (line 122) | func (web *Web) scheduleSavePostHandler(w http.ResponseWriter, r *http.R...
method renderSchedule (line 171) | func (web *Web) renderSchedule(w http.ResponseWriter, r *http.Request, e...
function getScheduleBlocks (line 220) | func getScheduleBlocks(r *http.Request) ([]model.ScheduleBlock, error) {
function getMatchType (line 247) | func getMatchType(r *http.Request) string {
FILE: web/setup_schedule_test.go
function TestSetupSchedule (line 13) | func TestSetupSchedule(t *testing.T) {
function TestSetupScheduleErrors (line 49) | func TestSetupScheduleErrors(t *testing.T) {
FILE: web/setup_settings.go
method settingsGetHandler (line 22) | func (web *Web) settingsGetHandler(w http.ResponseWriter, r *http.Reques...
method settingsPostHandler (line 31) | func (web *Web) settingsPostHandler(w http.ResponseWriter, r *http.Reque...
method saveDbHandler (line 173) | func (web *Web) saveDbHandler(w http.ResponseWriter, r *http.Request) {
method restoreDbHandler (line 190) | func (web *Web) restoreDbHandler(w http.ResponseWriter, r *http.Request) {
method clearDbHandler (line 259) | func (web *Web) clearDbHandler(w http.ResponseWriter, r *http.Request) {
method settingsPublishAlliancesHandler (line 310) | func (web *Web) settingsPublishAlliancesHandler(w http.ResponseWriter, r...
method settingsPublishAwardsHandler (line 330) | func (web *Web) settingsPublishAwardsHandler(w http.ResponseWriter, r *h...
method settingsPublishMatchesHandler (line 350) | func (web *Web) settingsPublishMatchesHandler(w http.ResponseWriter, r *...
method settingsPublishRankingsHandler (line 375) | func (web *Web) settingsPublishRankingsHandler(w http.ResponseWriter, r ...
method settingsPublishTeamsHandler (line 395) | func (web *Web) settingsPublishTeamsHandler(w http.ResponseWriter, r *ht...
method renderSettings (line 414) | func (web *Web) renderSettings(w http.ResponseWriter, r *http.Request, e...
method deleteMatchDataForType (line 432) | func (web *Web) deleteMatchDataForType(matchType model.MatchType) error {
FILE: web/setup_settings_test.go
function TestSetupSettings (line 19) | func TestSetupSettings(t *testing.T) {
function TestSetupSettingsDoubleElimination (line 45) | func TestSetupSettingsDoubleElimination(t *testing.T) {
function TestSetupSettingsInvalidValues (line 54) | func TestSetupSettingsInvalidValues(t *testing.T) {
function TestSetupSettingsClearDb (line 73) | func TestSetupSettingsClearDb(t *testing.T) {
function TestSetupSettingsBackupRestoreDb (line 178) | func TestSetupSettingsBackupRestoreDb(t *testing.T) {
function TestSetupSettingsPublishToTba (line 210) | func TestSetupSettingsPublishToTba(t *testing.T) {
method postFileHttpResponse (line 237) | func (web *Web) postFileHttpResponse(path string, paramName string, file...
FILE: web/setup_sponsor_slides.go
method sponsorSlidesGetHandler (line 15) | func (web *Web) sponsorSlidesGetHandler(w http.ResponseWriter, r *http.R...
method sponsorSlidesPostHandler (line 46) | func (web *Web) sponsorSlidesPostHandler(w http.ResponseWriter, r *http....
method reorderSponsorSlide (line 104) | func (web *Web) reorderSponsorSlide(id int, moveUp bool) error {
FILE: web/setup_sponsor_slides_test.go
function TestSetupSponsorSlides (line 12) | func TestSetupSponsorSlides(t *testing.T) {
FILE: web/setup_teams.go
constant wpaKeyLength (line 20) | wpaKeyLength = 8
method teamsGetHandler (line 26) | func (web *Web) teamsGetHandler(w http.ResponseWriter, r *http.Request) {
method teamsPostHandler (line 35) | func (web *Web) teamsPostHandler(w http.ResponseWriter, r *http.Request) {
method teamsRefreshHandler (line 76) | func (web *Web) teamsRefreshHandler(w http.ResponseWriter, r *http.Reque...
method teamsClearHandler (line 108) | func (web *Web) teamsClearHandler(w http.ResponseWriter, r *http.Request) {
method teamEditGetHandler (line 127) | func (web *Web) teamEditGetHandler(w http.ResponseWriter, r *http.Reques...
method teamEditPostHandler (line 160) | func (web *Web) teamEditPostHandler(w http.ResponseWriter, r *http.Reque...
method teamDeletePostHandler (line 202) | func (web *Web) teamDeletePostHandler(w http.ResponseWriter, r *http.Req...
method teamsGenerateWpaKeysHandler (line 231) | func (web *Web) teamsGenerateWpaKeysHandler(w http.ResponseWriter, r *ht...
method teamsUpdateProgressBarHandler (line 257) | func (web *Web) teamsUpdateProgressBarHandler(w http.ResponseWriter, r *...
method renderTeams (line 262) | func (web *Web) renderTeams(w http.ResponseWriter, r *http.Request, show...
method canModifyTeamList (line 287) | func (web *Web) canModifyTeamList() bool {
method populateOfficialTeamInfo (line 296) | func (web *Web) populateOfficialTeamInfo(team *model.Team) error {
FILE: web/setup_teams_test.go
function TestSetupTeams (line 16) | func TestSetupTeams(t *testing.T) {
function TestSetupTeamsDisallowModification (line 142) | func TestSetupTeamsDisallowModification(t *testing.T) {
function TestSetupTeamsBadReqest (line 175) | func TestSetupTeamsBadReqest(t *testing.T) {
function TestSetupTeamsWpaKeys (line 189) | func TestSetupTeamsWpaKeys(t *testing.T) {
function TestSetupTeamsProgress (line 221) | func TestSetupTeamsProgress(t *testing.T) {
FILE: web/twitch_display.go
method twitchDisplayHandler (line 15) | func (web *Web) twitchDisplayHandler(w http.ResponseWriter, r *http.Requ...
method twitchDisplayWebsocketHandler (line 36) | func (web *Web) twitchDisplayWebsocketHandler(w http.ResponseWriter, r *...
FILE: web/twitch_display_test.go
function TestTwitchDisplay (line 13) | func TestTwitchDisplay(t *testing.T) {
function TestTwitchDisplayWebsocket (line 21) | func TestTwitchDisplayWebsocket(t *testing.T) {
FILE: web/wall_display.go
method wallDisplayHandler (line 16) | func (web *Web) wallDisplayHandler(w http.ResponseWriter, r *http.Reques...
method wallDisplayWebsocketHandler (line 49) | func (web *Web) wallDisplayWebsocketHandler(w http.ResponseWriter, r *ht...
FILE: web/wall_display_test.go
function TestWallDisplay (line 13) | func TestWallDisplay(t *testing.T) {
function TestWallDisplayWebsocket (line 32) | func TestWallDisplayWebsocket(t *testing.T) {
FILE: web/web.go
constant sessionTokenCookie (line 23) | sessionTokenCookie = "session_token"
constant adminUser (line 24) | adminUser = "admin"
type Web (line 27) | type Web struct
method ServeWebInterface (line 90) | func (web *Web) ServeWebInterface(port int) {
method indexHandler (line 100) | func (web *Web) indexHandler(w http.ResponseWriter, r *http.Request) {
method newHandler (line 127) | func (web *Web) newHandler() http.Handler {
method parseFiles (line 247) | func (web *Web) parseFiles(filenames ...string) (*template.Template, e...
function NewWeb (line 32) | func NewWeb(arena *field.Arena) *Web {
function addNoCacheHeader (line 117) | func addNoCacheHeader(handler http.Handler) http.Handler {
function handleWebErr (line 241) | func handleWebErr(w http.ResponseWriter, err error) {
FILE: web/web_test.go
function TestIndex (line 18) | func TestIndex(t *testing.T) {
method getHttpResponse (line 26) | func (web *Web) getHttpResponse(path string) *httptest.ResponseRecorder {
method getHttpResponseWithHeaders (line 33) | func (web *Web) getHttpResponseWithHeaders(path string, headers map[stri...
method postHttpResponse (line 43) | func (web *Web) postHttpResponse(path string, body string) *httptest.Res...
method startTestServer (line 52) | func (web *Web) startTestServer() (*httptest.Server, string) {
function readWebsocketError (line 58) | func readWebsocketError(t *testing.T, ws *websocket.Websocket) string {
function readWebsocketType (line 67) | func readWebsocketType(t *testing.T, ws *websocket.Websocket, expectedMe...
function readWebsocketMultiple (line 75) | func readWebsocketMultiple(t *testing.T, ws *websocket.Websocket, count ...
function setupTestWeb (line 86) | func setupTestWeb(t *testing.T) *Web {
FILE: web/webpage_display.go
method webpageDisplayHandler (line 15) | func (web *Web) webpageDisplayHandler(w http.ResponseWriter, r *http.Req...
method webpageDisplayWebsocketHandler (line 36) | func (web *Web) webpageDisplayWebsocketHandler(w http.ResponseWriter, r ...
FILE: web/webpage_display_test.go
function TestWebpageDisplay (line 13) | func TestWebpageDisplay(t *testing.T) {
function TestWebpageDisplayWebsocket (line 21) | func TestWebpageDisplayWebsocket(t *testing.T) {
FILE: websocket/notifier.go
constant notifyBufferSize (line 14) | notifyBufferSize = 5
type Notifier (line 16) | type Notifier struct
method Notify (line 36) | func (notifier *Notifier) Notify() {
method NotifyWithMessage (line 42) | func (notifier *Notifier) NotifyWithMessage(messageBody any) {
method notifyListener (line 52) | func (notifier *Notifier) notifyListener(listener chan messageEnvelope...
method listen (line 72) | func (notifier *Notifier) listen() chan messageEnvelope {
method getMessageBody (line 82) | func (notifier *Notifier) getMessageBody() any {
type messageEnvelope (line 23) | type messageEnvelope struct
function NewNotifier (line 28) | func NewNotifier(messageType string, messageProducer func() any) *Notifi...
FILE: websocket/notifier_test.go
function TestNotifier (line 13) | func TestNotifier(t *testing.T) {
function TestNotifyMultipleListeners (line 57) | func TestNotifyMultipleListeners(t *testing.T) {
function generateTestMessage (line 88) | func generateTestMessage() any {
FILE: websocket/websocket.go
constant pingInterval (line 21) | pingInterval = time.Second * 10
type Websocket (line 24) | type Websocket struct
method Close (line 49) | func (ws *Websocket) Close() error {
method Read (line 53) | func (ws *Websocket) Read() (string, any, error) {
method ReadWithTimeout (line 72) | func (ws *Websocket) ReadWithTimeout(timeout time.Duration) (string, a...
method Write (line 92) | func (ws *Websocket) Write(messageType string, data any) error {
method WriteNotifier (line 105) | func (ws *Websocket) WriteNotifier(notifier *Notifier) error {
method WriteError (line 109) | func (ws *Websocket) WriteError(errorMessage string) error {
method HandleNotifiers (line 114) | func (ws *Websocket) HandleNotifiers(notifiers ...*Notifier) {
type Message (line 29) | type Message struct
function NewWebsocket (line 37) | func NewWebsocket(w http.ResponseWriter, r *http.Request) (*Websocket, e...
function NewTestWebsocket (line 45) | func NewTestWebsocket(conn *websocket.Conn) *Websocket {
FILE: websocket/websocket_test.go
function TestWebsocket (line 16) | func TestWebsocket(t *testing.T) {
function assertMessage (line 110) | func assertMessage(t *testing.T, ws *Websocket, expectedMessageType stri...
Condensed preview — 1621 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,331K chars).
[
{
"path": ".editorconfig",
"chars": 15115,
"preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_newline = false\nmax_"
},
{
"path": ".github/workflows/release.yml",
"chars": 4132,
"preview": "on:\n push:\n tags:\n - \"v*\"\nname: Create Release\njobs:\n create_release:\n runs-on: ubuntu-latest\n env:\n "
},
{
"path": ".github/workflows/test.yml",
"chars": 419,
"preview": "on: [ push, pull_request ]\nname: Build/Test\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - name: Install Go"
},
{
"path": ".gitignore",
"chars": 343,
"preview": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\ncheesy-arena\n\n# Folders\n_obj\n_test\n.idea\n"
},
{
"path": "AGENTS.md",
"chars": 2072,
"preview": "# Repository Guidelines\n\n## Project Structure & Module Organization\n`main.go` is the entry point for the Go web server. "
},
{
"path": "LICENSE",
"chars": 1288,
"preview": "Copyright (c) 2014, Team 254\nAll rights reserved.\n\nThis software may be used and redistributed subject to the following "
},
{
"path": "README.md",
"chars": 6580,
"preview": "Cheesy Arena [](https://git"
},
{
"path": "coverage",
"chars": 88,
"preview": "go test -coverprofile=coverage.out ./... && sleep 1 && go tool cover -html=coverage.out\n"
},
{
"path": "field/arena.go",
"chars": 41661,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Functions for c"
},
{
"path": "field/arena_notifiers.go",
"chars": 12595,
"preview": "// Copyright 2018 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Contains config"
},
{
"path": "field/arena_test.go",
"chars": 53884,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage field\n\nimpor"
},
{
"path": "field/display.go",
"chars": 8072,
"preview": "// Copyright 2018 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model represent"
},
{
"path": "field/display_test.go",
"chars": 7455,
"preview": "// Copyright 2018 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage field\n\nimpor"
},
{
"path": "field/driver_station_connection.go",
"chars": 17119,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and metho"
},
{
"path": "field/driver_station_connection_test.go",
"chars": 9725,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage field\n\nimpor"
},
{
"path": "field/event_status.go",
"chars": 4434,
"preview": "// Copyright 2020 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and funct"
},
{
"path": "field/event_status_test.go",
"chars": 7386,
"preview": "// Copyright 2020 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage field\n\nimpor"
},
{
"path": "field/fake_plc_test.go",
"chars": 2437,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Contains a fake"
},
{
"path": "field/realtime_score.go",
"chars": 445,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model represent"
},
{
"path": "field/scoring_panel_registry.go",
"chars": 2442,
"preview": "// Copyright 2019 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model represent"
},
{
"path": "field/scoring_panel_registry_test.go",
"chars": 1860,
"preview": "// Copyright 2019 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage field\n\nimpor"
},
{
"path": "field/team_match_log.go",
"chars": 2070,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Utilities for l"
},
{
"path": "field/team_sign.go",
"chars": 14468,
"preview": "// Copyright 2024 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Models and logi"
},
{
"path": "field/team_sign_test.go",
"chars": 7765,
"preview": "// Copyright 2024 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage field\n\nimpor"
},
{
"path": "field/test_helpers.go",
"chars": 773,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Helper methods "
},
{
"path": "fix_avatar_colors_for_overlay",
"chars": 643,
"preview": "# This script replaces a single color in the team avatars with a second one, to fix conflicts with chroma/luma key.\n# By"
},
{
"path": "font/helvetica.json",
"chars": 1088,
"preview": "{\"Tp\":\"Core\",\"Name\":\"Helvetica\",\"Up\":-100,\"Ut\":50,\"Cw\":[278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,"
},
{
"path": "font/helveticab.json",
"chars": 1092,
"preview": "{\"Tp\":\"Core\",\"Name\":\"Helvetica-Bold\",\"Up\":-100,\"Ut\":50,\"Cw\":[278,278,278,278,278,278,278,278,278,278,278,278,278,278,278"
},
{
"path": "font/helveticabi.json",
"chars": 1099,
"preview": "{\"Tp\":\"Core\",\"Name\":\"Helvetica-BoldOblique\",\"Up\":-100,\"Ut\":50,\"Cw\":[278,278,278,278,278,278,278,278,278,278,278,278,278,"
},
{
"path": "font/helveticai.json",
"chars": 1096,
"preview": "{\"Tp\":\"Core\",\"Name\":\"Helvetica-Oblique\",\"Up\":-100,\"Ut\":50,\"Cw\":[278,278,278,278,278,278,278,278,278,278,278,278,278,278,"
},
{
"path": "game/foul.go",
"chars": 720,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model of a foul"
},
{
"path": "game/match_sounds.go",
"chars": 1216,
"preview": "// Copyright 2019 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Game-specific a"
},
{
"path": "game/match_timing.go",
"chars": 986,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Game-specific p"
},
{
"path": "game/ranking_fields.go",
"chars": 2713,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Game-specific f"
},
{
"path": "game/ranking_fields_test.go",
"chars": 3864,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage game\n\nimport"
},
{
"path": "game/reef.go",
"chars": 4197,
"preview": "// Copyright 2025 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Scoring logic f"
},
{
"path": "game/reef_test.go",
"chars": 9786,
"preview": "// Copyright 2025 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage game\n\nimport"
},
{
"path": "game/rule.go",
"chars": 7561,
"preview": "// Copyright 2020 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model of a game"
},
{
"path": "game/rule_test.go",
"chars": 554,
"preview": "// Copyright 2020 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage game\n\nimport"
},
{
"path": "game/score.go",
"chars": 4882,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model represent"
},
{
"path": "game/score_summary.go",
"chars": 2078,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model represent"
},
{
"path": "game/score_summary_test.go",
"chars": 2715,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage game\n\nimport"
},
{
"path": "game/score_test.go",
"chars": 15186,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage game\n\nimport"
},
{
"path": "game/test_helpers.go",
"chars": 1747,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Helper methods "
},
{
"path": "go.mod",
"chars": 600,
"preview": "module github.com/Team254/cheesy-arena\n\ngo 1.23.0\n\nrequire (\n\tgithub.com/dchest/uniuri v1.2.0\n\tgithub.com/goburrow/modbu"
},
{
"path": "go.sum",
"chars": 4098,
"preview": "github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/davecgh/go-spew v1"
},
{
"path": "main.go",
"chars": 851,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\n// Go version 1.22 o"
},
{
"path": "model/alliance.go",
"chars": 3250,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/alliance_test.go",
"chars": 3830,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/award.go",
"chars": 1502,
"preview": "// Copyright 2019 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/award_test.go",
"chars": 2398,
"preview": "// Copyright 2019 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/database.go",
"chars": 3620,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Functions for m"
},
{
"path": "model/database_test.go",
"chars": 369,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/event_settings.go",
"chars": 5532,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/event_settings_test.go",
"chars": 1776,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/judging_slot.go",
"chars": 1013,
"preview": "// Copyright 2025 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/judging_slot_test.go",
"chars": 1982,
"preview": "// Copyright 2025 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/lower_third.go",
"chars": 1903,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/lower_third_test.go",
"chars": 2856,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/match.go",
"chars": 4472,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/match_result.go",
"chars": 2706,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/match_result_test.go",
"chars": 2063,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/match_test.go",
"chars": 5302,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/matchtype_string.go",
"chars": 693,
"preview": "// Code generated by \"stringer -type=MatchType\"; DO NOT EDIT.\n\npackage model\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"inval"
},
{
"path": "model/ranking.go",
"chars": 1467,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/ranking_test.go",
"chars": 1554,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/schedule_block.go",
"chars": 1588,
"preview": "// Copyright 2018 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/schedule_block_test.go",
"chars": 1604,
"preview": "// Copyright 2018 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/scheduled_break.go",
"chars": 2352,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/scheduled_break_test.go",
"chars": 3004,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/sponsor_slide.go",
"chars": 1597,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: nick@team254.com (Nick Eyre)\n//\n// Model and datastore CRUD "
},
{
"path": "model/sponsor_slide_test.go",
"chars": 1544,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/table.go",
"chars": 6883,
"preview": "// Copyright 2021 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Defines a \"tabl"
},
{
"path": "model/table_test.go",
"chars": 5172,
"preview": "// Copyright 2021 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/team.go",
"chars": 1296,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/team_test.go",
"chars": 1804,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "model/test_helpers.go",
"chars": 1209,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Helper methods "
},
{
"path": "model/user_session.go",
"chars": 960,
"preview": "// Copyright 2019 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Model and datas"
},
{
"path": "model/user_session_test.go",
"chars": 1251,
"preview": "// Copyright 2019 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage model\n\nimpor"
},
{
"path": "network/access_point.go",
"chars": 9003,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Methods for con"
},
{
"path": "network/access_point_test.go",
"chars": 6410,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage network\n\nimp"
},
{
"path": "network/sccswitch.go",
"chars": 3866,
"preview": "// Copyright 2025 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Methods for con"
},
{
"path": "network/sccswitch_test.go",
"chars": 3681,
"preview": "// Copyright 2025 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage network\n\nimp"
},
{
"path": "network/switch.go",
"chars": 4363,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Methods for con"
},
{
"path": "network/switch_test.go",
"chars": 4777,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage network\n\nimp"
},
{
"path": "network/testdata/iwinfo_0_teams.txt",
"chars": 3470,
"preview": "wlan0 ESSID: \"no-team-1\"\n Access Point: 00:25:9C:14:65:8A\n Mode: Master Channel: 161 (5.805 GHz)\n"
},
{
"path": "network/testdata/iwinfo_2_teams.txt",
"chars": 3453,
"preview": "wlan0 ESSID: \"no-team-1\"\n Access Point: 00:25:9C:14:65:8A\n Mode: Master Channel: 161 (5.805 GHz)\n"
},
{
"path": "network/testdata/iwinfo_6_teams.txt",
"chars": 3431,
"preview": "wlan0 ESSID: \"254\"\n Access Point: 00:25:9C:14:65:8A\n Mode: Master Channel: 161 (5.805 GHz)\n "
},
{
"path": "network/testdata/iwinfo_invalid.txt",
"chars": 2452,
"preview": "wlan0 ESSID: \"254\"\n Access Point: 00:25:9C:14:65:8A\n Mode: Master Channel: 161 (5.805 GHz)\n "
},
{
"path": "partner/blackmagic.go",
"chars": 1822,
"preview": "// Copyright 2024 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Client for inte"
},
{
"path": "partner/blackmagic_test.go",
"chars": 915,
"preview": "// Copyright 2024 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage partner\n\nimp"
},
{
"path": "partner/companion.go",
"chars": 2891,
"preview": "// Copyright 2025 Team 254. All Rights Reserved.\n// Author: kyle@team2481.com (Kyle Waremburg)\n//\n// Client for interfac"
},
{
"path": "partner/companion_test.go",
"chars": 3031,
"preview": "// Copyright 2025 Team 254. All Rights Reserved.\n// Author: kyle@team2481.com (Kyle Waremburg)\n//\n// Tests for the Compa"
},
{
"path": "partner/nexus.go",
"chars": 2320,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Methods for pul"
},
{
"path": "partner/nexus_test.go",
"chars": 2052,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage partner\n\nimp"
},
{
"path": "partner/tba.go",
"chars": 23509,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Methods for pub"
},
{
"path": "partner/tba_test.go",
"chars": 5704,
"preview": "// Copyright 2014 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage partner\n\nimp"
},
{
"path": "playoff/alliance_source.go",
"chars": 3118,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Represents how "
},
{
"path": "playoff/break_spec.go",
"chars": 263,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Represents a sc"
},
{
"path": "playoff/double_elimination.go",
"chars": 5430,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Defines the tou"
},
{
"path": "playoff/double_elimination_test.go",
"chars": 10694,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage playoff\n\nimp"
},
{
"path": "playoff/match_group.go",
"chars": 3336,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Interface repre"
},
{
"path": "playoff/match_group_test.go",
"chars": 2694,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage playoff\n\nimp"
},
{
"path": "playoff/matchup.go",
"chars": 7591,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Models and logi"
},
{
"path": "playoff/matchup_test.go",
"chars": 5510,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage playoff\n\nimp"
},
{
"path": "playoff/playoff_match_result.go",
"chars": 279,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Represents the "
},
{
"path": "playoff/playoff_tournament.go",
"chars": 8868,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Models and logi"
},
{
"path": "playoff/playoff_tournament_test.go",
"chars": 10526,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage playoff\n\nimp"
},
{
"path": "playoff/single_elimination.go",
"chars": 10226,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Defines the tou"
},
{
"path": "playoff/single_elimination_test.go",
"chars": 31562,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage playoff\n\nimp"
},
{
"path": "playoff/test_helpers.go",
"chars": 4790,
"preview": "// Copyright 2022 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Helper methods "
},
{
"path": "plc/armorblock_string.go",
"chars": 742,
"preview": "// Code generated by \"stringer -type=armorBlock\"; DO NOT EDIT.\n\npackage plc\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invali"
},
{
"path": "plc/coil_string.go",
"chars": 1215,
"preview": "// Code generated by \"stringer -type=coil\"; DO NOT EDIT.\n\npackage plc\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid arra"
},
{
"path": "plc/fake_modbus_client_test.go",
"chars": 2439,
"preview": "// Copyright 2023 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Contains a fake"
},
{
"path": "plc/input_string.go",
"chars": 1278,
"preview": "// Code generated by \"stringer -type=input\"; DO NOT EDIT.\n\npackage plc\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid arr"
},
{
"path": "plc/plc.go",
"chars": 11866,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n//\n// Methods for int"
},
{
"path": "plc/plc_test.go",
"chars": 16548,
"preview": "// Copyright 2017 Team 254. All Rights Reserved.\n// Author: pat@patfairbank.com (Patrick Fairbank)\n\npackage plc\n\nimport "
},
{
"path": "plc/register_string.go",
"chars": 728,
"preview": "// Code generated by \"stringer -type=register\"; DO NOT EDIT.\n\npackage plc\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid "
},
{
"path": "schedules/100_1.csv",
"chars": 502,
"preview": "44,0,27,0,3,0,58,0,82,0,93,0\n52,0,54,0,90,0,35,0,83,0,74,0\n94,0,72,0,70,0,46,0,53,0,33,0\n17,0,92,0,86,0,50,0,60,0,76,0\n8"
},
{
"path": "schedules/100_10.csv",
"chars": 4930,
"preview": "50,0,86,0,34,0,94,0,89,0,19,0\n100,0,10,0,79,0,8,0,4,0,56,0\n91,0,47,0,17,0,68,0,46,0,96,0\n74,0,64,0,38,0,57,0,14,0,45,0\n2"
},
{
"path": "schedules/100_11.csv",
"chars": 5431,
"preview": "46,0,74,0,97,0,20,0,34,0,38,0\n83,0,53,0,6,0,23,0,75,0,42,0\n48,0,87,0,43,0,45,0,44,0,33,0\n100,0,37,0,66,0,22,0,28,0,76,0\n"
},
{
"path": "schedules/100_12.csv",
"chars": 5904,
"preview": "22,0,65,0,43,0,10,0,30,0,53,0\n15,0,84,0,7,0,69,0,28,0,77,0\n33,0,47,0,48,0,29,0,1,0,78,0\n25,0,74,0,91,0,55,0,99,0,4,0\n64,"
},
{
"path": "schedules/100_13.csv",
"chars": 6406,
"preview": "28,0,96,0,29,0,10,0,32,0,33,0\n16,0,37,0,57,0,49,0,9,0,88,0\n25,0,1,0,40,0,72,0,68,0,100,0\n98,0,55,0,7,0,24,0,59,0,14,0\n82"
},
{
"path": "schedules/100_14.csv",
"chars": 6908,
"preview": "58,0,52,0,10,0,94,0,17,0,24,0\n20,0,81,0,61,0,21,0,26,0,6,0\n35,0,89,0,75,0,74,0,31,0,76,0\n78,0,48,0,12,0,49,0,38,0,66,0\n2"
},
{
"path": "schedules/100_2.csv",
"chars": 1004,
"preview": "58,0,44,0,80,0,95,0,23,0,79,0\n88,0,17,0,70,0,30,0,68,0,43,0\n10,0,38,0,66,0,51,0,19,0,98,0\n20,0,67,0,40,0,21,0,25,0,63,0\n"
},
{
"path": "schedules/100_3.csv",
"chars": 1476,
"preview": "15,0,59,0,25,0,16,0,5,0,64,0\n73,0,90,0,52,0,30,0,39,0,67,0\n86,0,13,0,11,0,65,0,31,0,48,0\n60,0,63,0,45,0,35,0,38,0,95,0\n3"
},
{
"path": "schedules/100_4.csv",
"chars": 1978,
"preview": "11,0,47,0,34,0,82,0,56,0,25,0\n51,0,80,0,50,0,1,0,4,0,63,0\n86,0,37,0,100,0,89,0,72,0,39,0\n2,0,9,0,29,0,92,0,3,0,52,0\n20,0"
},
{
"path": "schedules/100_5.csv",
"chars": 2479,
"preview": "64,0,13,0,1,0,70,0,14,0,33,0\n22,0,72,0,53,0,36,0,92,0,71,0\n74,0,42,0,7,0,12,0,28,0,68,0\n96,0,11,0,51,0,89,0,77,0,5,0\n18,"
},
{
"path": "schedules/100_6.csv",
"chars": 2952,
"preview": "25,0,90,0,26,0,62,0,80,0,97,0\n54,0,27,0,52,0,100,0,20,0,1,0\n79,0,74,0,76,0,34,0,91,0,30,0\n31,0,7,0,44,0,24,0,32,0,61,0\n9"
},
{
"path": "schedules/100_7.csv",
"chars": 3454,
"preview": "22,0,72,0,93,0,76,0,66,0,36,0\n3,0,39,0,33,0,26,0,99,0,74,0\n7,0,38,0,40,0,37,0,32,0,82,0\n77,0,57,0,83,0,42,0,64,0,80,0\n69"
},
{
"path": "schedules/100_8.csv",
"chars": 3957,
"preview": "57,0,81,0,2,0,82,0,80,0,35,0\n20,0,96,0,59,0,12,0,76,0,45,0\n74,0,31,0,5,0,8,0,87,0,69,0\n48,0,64,0,40,0,86,0,16,0,67,0\n19,"
},
{
"path": "schedules/100_9.csv",
"chars": 4428,
"preview": "45,0,20,0,86,0,91,0,6,0,34,0\n56,0,43,0,67,0,31,0,98,0,3,0\n74,0,97,0,95,0,60,0,4,0,69,0\n18,0,89,0,78,0,10,0,88,0,36,0\n13,"
},
{
"path": "schedules/10_1.csv",
"chars": 50,
"preview": "3,0,9,0,10,0,4,0,6,0,2,0\n8,0,1,0,4,1,7,0,5,0,10,1\n"
},
{
"path": "schedules/10_10.csv",
"chars": 418,
"preview": "3,0,1,0,10,0,7,0,4,0,6,0\n5,0,2,0,4,0,8,0,9,0,6,0\n5,0,8,0,3,0,10,0,7,0,2,0\n3,0,2,0,9,0,8,0,6,0,1,0\n7,1,10,0,4,0,9,1,5,0,1"
},
{
"path": "schedules/10_11.csv",
"chars": 467,
"preview": "3,0,2,0,9,0,5,0,10,0,4,0\n9,0,6,0,8,0,7,0,10,0,1,0\n8,0,7,0,4,0,6,0,5,0,3,0\n1,0,10,0,5,0,2,0,4,0,8,0\n9,1,1,1,6,0,2,1,7,0,3"
},
{
"path": "schedules/10_12.csv",
"chars": 492,
"preview": "5,0,10,0,1,0,8,0,2,0,6,0\n4,0,3,0,1,0,9,0,7,0,10,0\n3,0,6,0,9,0,4,0,2,0,5,0\n8,0,4,0,3,0,1,0,7,0,2,0\n8,0,7,0,9,0,5,0,10,0,6"
},
{
"path": "schedules/10_13.csv",
"chars": 542,
"preview": "10,0,6,0,2,0,1,0,9,0,4,0\n4,0,5,0,8,0,7,0,6,0,3,0\n2,0,7,0,5,0,8,0,9,0,1,0\n10,0,1,0,9,0,3,0,4,0,8,0\n3,0,2,0,7,1,6,0,5,0,10"
},
{
"path": "schedules/10_14.csv",
"chars": 590,
"preview": "4,0,8,0,3,0,7,0,9,0,1,0\n6,0,2,0,1,0,5,0,10,0,8,0\n9,0,10,0,3,0,6,0,4,0,5,0\n5,0,2,0,6,0,4,0,10,0,7,0\n1,1,3,1,8,0,2,1,9,1,7"
},
{
"path": "schedules/10_2.csv",
"chars": 99,
"preview": "4,0,6,0,9,0,10,0,2,0,7,0\n1,0,7,0,3,0,8,0,5,0,2,0\n5,0,4,0,10,0,6,0,8,0,1,0\n3,0,6,1,4,1,9,0,10,1,1,1\n"
},
{
"path": "schedules/10_3.csv",
"chars": 123,
"preview": "5,0,4,0,2,0,6,0,1,0,10,0\n7,0,3,0,6,0,8,0,9,0,1,0\n3,0,5,0,8,0,10,0,7,0,4,0\n2,0,10,0,8,0,9,0,7,0,5,0\n2,0,6,0,9,0,4,0,1,0,3"
},
{
"path": "schedules/10_4.csv",
"chars": 172,
"preview": "7,0,4,0,9,0,3,0,2,0,8,0\n6,0,3,0,5,0,10,0,1,0,2,0\n8,0,1,0,4,0,7,0,6,0,10,0\n5,0,10,0,4,0,3,0,9,0,1,0\n7,1,8,0,6,0,2,1,5,0,9"
},
{
"path": "schedules/10_5.csv",
"chars": 221,
"preview": "7,0,8,0,4,0,10,0,2,0,1,0\n3,0,9,0,4,0,8,0,5,0,6,0\n1,0,9,0,10,0,2,0,3,0,5,0\n3,0,1,0,6,0,7,0,4,0,10,0\n8,1,2,0,7,1,9,0,5,1,6"
},
{
"path": "schedules/10_6.csv",
"chars": 246,
"preview": "1,0,6,0,10,0,5,0,8,0,2,0\n4,0,3,0,5,0,6,0,7,0,9,0\n10,0,9,0,4,0,1,0,2,0,7,0\n6,0,7,0,3,0,8,0,1,0,9,0\n2,0,8,0,10,0,3,0,4,0,5"
},
{
"path": "schedules/10_7.csv",
"chars": 295,
"preview": "1,0,5,0,10,0,8,0,7,0,9,0\n6,0,1,0,3,0,5,0,4,0,2,0\n3,0,7,0,8,0,9,0,6,0,2,0\n4,0,5,0,8,0,2,0,7,0,10,0\n4,1,10,0,6,0,1,1,3,0,9"
},
{
"path": "schedules/10_8.csv",
"chars": 344,
"preview": "4,0,8,0,2,0,1,0,9,0,7,0\n5,0,3,0,7,0,10,0,4,0,6,0\n1,0,2,0,9,0,5,0,6,0,10,0\n8,0,6,0,7,0,3,0,10,0,1,0\n9,0,8,1,5,1,4,1,2,1,3"
},
{
"path": "schedules/10_9.csv",
"chars": 369,
"preview": "8,0,3,0,6,0,7,0,2,0,9,0\n5,0,10,0,6,0,8,0,4,0,1,0\n10,0,2,0,3,0,5,0,7,0,1,0\n6,0,4,0,7,0,9,0,10,0,2,0\n9,0,1,0,4,0,3,0,5,0,8"
},
{
"path": "schedules/11_1.csv",
"chars": 50,
"preview": "4,0,9,0,6,0,8,0,5,0,1,0\n10,0,7,0,2,0,11,0,3,0,4,1\n"
},
{
"path": "schedules/11_10.csv",
"chars": 476,
"preview": "1,0,2,0,11,0,3,0,8,0,6,0\n7,0,10,0,9,0,4,0,5,0,8,0\n10,0,4,0,3,0,7,0,1,0,6,0\n5,0,2,0,6,0,11,0,9,0,4,0\n9,1,3,1,5,0,11,0,1,1"
},
{
"path": "schedules/11_11.csv",
"chars": 528,
"preview": "2,0,1,0,7,0,3,0,5,0,6,0\n9,0,10,0,2,0,4,0,8,0,11,0\n7,0,4,0,5,0,8,0,11,0,6,0\n4,0,10,0,3,0,1,0,9,0,6,0\n9,1,8,1,7,0,5,1,11,1"
},
{
"path": "schedules/11_12.csv",
"chars": 552,
"preview": "8,0,3,0,5,0,11,0,4,0,7,0\n1,0,6,0,2,0,4,0,10,0,9,0\n7,0,11,0,6,0,1,0,5,0,10,0\n8,0,7,0,3,0,2,0,9,0,1,0\n4,0,2,0,5,0,6,0,3,0,"
},
{
"path": "schedules/11_13.csv",
"chars": 603,
"preview": "9,0,1,0,10,0,7,0,11,0,5,0\n8,0,4,0,2,0,6,0,11,0,3,0\n9,0,6,0,4,0,1,0,5,0,3,0\n8,0,6,0,7,0,10,0,2,0,9,0\n5,0,1,0,11,1,2,0,3,0"
},
{
"path": "schedules/11_14.csv",
"chars": 652,
"preview": "10,0,5,0,4,0,1,0,8,0,11,0\n2,0,3,0,11,0,9,0,7,0,6,0\n7,0,6,0,3,0,1,0,8,0,2,0\n4,0,10,0,11,0,9,0,5,0,7,0\n5,1,3,0,2,0,1,0,4,1"
},
{
"path": "schedules/11_2.csv",
"chars": 101,
"preview": "4,0,10,0,2,0,11,0,5,0,9,0\n7,0,3,0,1,0,8,0,6,0,4,0\n5,0,8,0,10,0,9,0,7,0,6,0\n1,0,11,0,10,1,3,0,2,0,9,1\n"
},
{
"path": "schedules/11_3.csv",
"chars": 150,
"preview": "5,0,8,0,4,0,3,0,1,0,11,0\n7,0,6,0,9,0,4,0,10,0,2,0\n9,0,2,0,5,0,8,0,3,0,6,0\n6,0,11,0,10,0,1,0,7,0,8,0\n2,0,7,0,3,0,10,0,5,0"
},
{
"path": "schedules/11_4.csv",
"chars": 200,
"preview": "6,0,8,0,2,0,4,0,10,0,5,0\n9,0,1,0,3,0,11,0,8,0,7,0\n1,0,6,0,4,0,7,0,5,0,9,0\n10,0,11,0,1,0,3,0,4,0,2,0\n3,1,5,1,11,0,9,1,6,1"
},
{
"path": "schedules/11_5.csv",
"chars": 252,
"preview": "4,0,1,0,10,0,8,0,7,0,2,0\n5,0,6,0,3,0,8,0,11,0,9,0\n6,0,9,0,1,0,5,0,10,0,11,0\n7,0,2,0,6,0,3,0,4,0,9,0\n2,1,3,1,11,1,10,1,8,"
},
{
"path": "schedules/11_6.csv",
"chars": 276,
"preview": "4,0,9,0,6,0,11,0,5,0,7,0\n8,0,9,0,2,0,1,0,3,0,10,0\n5,0,8,0,3,0,4,0,10,0,11,0\n5,0,1,0,6,0,2,0,7,0,8,0\n7,0,3,0,4,0,2,0,11,0"
},
{
"path": "schedules/11_7.csv",
"chars": 327,
"preview": "8,0,11,0,7,0,3,0,4,0,10,0\n1,0,9,0,5,0,6,0,11,0,2,0\n7,0,4,0,2,0,1,0,10,0,9,0\n3,0,1,0,6,0,5,0,10,0,8,0\n11,1,7,0,6,0,8,0,2,"
},
{
"path": "schedules/11_8.csv",
"chars": 376,
"preview": "1,0,7,0,5,0,11,0,3,0,2,0\n9,0,8,0,6,0,10,0,4,0,5,0\n7,0,3,0,8,0,4,0,10,0,11,0\n1,0,6,0,4,0,2,0,7,0,9,0\n6,1,11,0,2,0,10,0,3,"
},
{
"path": "schedules/11_9.csv",
"chars": 427,
"preview": "7,0,2,0,10,0,3,0,5,0,1,0\n11,0,8,0,9,0,6,0,4,0,3,0\n11,0,6,0,5,0,2,0,1,0,4,0\n8,0,6,0,7,0,9,0,10,0,1,0\n7,1,11,1,5,0,4,0,9,1"
},
{
"path": "schedules/12_1.csv",
"chars": 51,
"preview": "11,0,9,0,12,0,6,0,2,0,5,0\n4,0,3,0,1,0,10,0,8,0,7,0\n"
},
{
"path": "schedules/12_10.csv",
"chars": 510,
"preview": "8,0,4,0,5,0,12,0,6,0,1,0\n2,0,9,0,3,0,10,0,11,0,7,0\n7,0,12,0,10,0,9,0,4,0,1,0\n3,0,8,0,11,0,6,0,5,0,2,0\n10,0,8,0,6,0,7,0,1"
},
{
"path": "schedules/12_11.csv",
"chars": 561,
"preview": "12,0,9,0,3,0,2,0,1,0,5,0\n11,0,10,0,6,0,7,0,4,0,8,0\n5,0,1,0,3,0,10,0,6,0,8,0\n9,0,4,0,11,0,12,0,2,0,7,0\n8,0,12,0,4,0,1,0,7"
},
{
"path": "schedules/12_12.csv",
"chars": 612,
"preview": "10,0,1,0,9,0,5,0,3,0,8,0\n4,0,2,0,6,0,12,0,7,0,11,0\n8,0,5,0,1,0,11,0,3,0,2,0\n4,0,12,0,10,0,6,0,9,0,7,0\n3,0,7,0,1,0,12,0,2"
},
{
"path": "schedules/12_13.csv",
"chars": 663,
"preview": "10,0,7,0,8,0,1,0,6,0,4,0\n9,0,5,0,3,0,2,0,12,0,11,0\n7,0,12,0,10,0,11,0,9,0,6,0\n2,0,5,0,8,0,3,0,1,0,4,0\n10,0,2,0,12,0,8,0,"
},
{
"path": "schedules/12_14.csv",
"chars": 714,
"preview": "2,0,9,0,12,0,10,0,8,0,6,0\n4,0,1,0,5,0,7,0,11,0,3,0\n9,0,5,0,6,0,1,0,10,0,12,0\n2,0,11,0,3,0,7,0,8,0,4,0\n11,0,3,0,9,0,1,0,6"
},
{
"path": "schedules/12_2.csv",
"chars": 102,
"preview": "12,0,5,0,2,0,1,0,8,0,7,0\n4,0,11,0,6,0,3,0,10,0,9,0\n6,0,1,0,3,0,8,0,12,0,4,0\n2,0,7,0,10,0,5,0,9,0,11,0\n"
},
{
"path": "schedules/12_3.csv",
"chars": 153,
"preview": "1,0,11,0,4,0,7,0,3,0,6,0\n12,0,10,0,2,0,8,0,9,0,5,0\n9,0,7,0,12,0,2,0,4,0,3,0\n8,0,6,0,11,0,5,0,1,0,10,0\n3,0,8,0,10,0,4,0,5"
},
{
"path": "schedules/12_4.csv",
"chars": 204,
"preview": "8,0,2,0,4,0,9,0,11,0,3,0\n6,0,10,0,7,0,12,0,1,0,5,0\n7,0,9,0,8,0,4,0,6,0,12,0\n3,0,1,0,10,0,11,0,5,0,2,0\n5,0,4,0,9,0,1,0,7,"
},
{
"path": "schedules/12_5.csv",
"chars": 255,
"preview": "8,0,6,0,12,0,1,0,10,0,9,0\n3,0,11,0,7,0,5,0,2,0,4,0\n4,0,9,0,6,0,2,0,12,0,3,0\n5,0,7,0,10,0,11,0,8,0,1,0\n6,0,1,0,5,0,7,0,4,"
},
{
"path": "schedules/12_6.csv",
"chars": 306,
"preview": "3,0,4,0,10,0,11,0,9,0,5,0\n2,0,7,0,12,0,8,0,6,0,1,0\n12,0,11,0,6,0,2,0,5,0,10,0\n3,0,8,0,4,0,1,0,9,0,7,0\n5,0,12,0,1,0,7,0,1"
},
{
"path": "schedules/12_7.csv",
"chars": 357,
"preview": "8,0,4,0,2,0,1,0,3,0,5,0\n7,0,6,0,9,0,11,0,12,0,10,0\n10,0,9,0,8,0,1,0,2,0,12,0\n3,0,7,0,4,0,5,0,11,0,6,0\n4,0,9,0,1,0,5,0,10"
},
{
"path": "schedules/12_8.csv",
"chars": 408,
"preview": "8,0,2,0,10,0,7,0,6,0,11,0\n9,0,5,0,12,0,3,0,4,0,1,0\n1,0,3,0,9,0,10,0,2,0,6,0\n8,0,7,0,12,0,4,0,5,0,11,0\n11,0,4,0,2,0,9,0,6"
},
{
"path": "schedules/12_9.csv",
"chars": 459,
"preview": "9,0,11,0,7,0,6,0,12,0,1,0\n3,0,10,0,8,0,5,0,2,0,4,0\n2,0,4,0,11,0,10,0,7,0,9,0\n1,0,8,0,12,0,3,0,6,0,5,0\n5,0,4,0,12,0,11,0,"
},
{
"path": "schedules/13_1.csv",
"chars": 78,
"preview": "12,0,6,0,9,0,7,0,10,0,5,0\n11,0,13,0,3,0,4,0,2,0,8,0\n1,0,4,1,3,1,11,1,13,1,2,1\n"
},
{
"path": "schedules/13_10.csv",
"chars": 569,
"preview": "10,0,6,0,1,0,7,0,13,0,11,0\n3,0,5,0,12,0,4,0,2,0,9,0\n8,0,2,0,4,0,13,0,10,0,5,0\n9,0,3,0,12,0,6,0,8,0,11,0\n7,0,10,0,9,0,5,0"
},
{
"path": "schedules/13_11.csv",
"chars": 620,
"preview": "2,0,9,0,4,0,3,0,6,0,8,0\n1,0,12,0,10,0,5,0,7,0,13,0\n3,0,2,0,5,0,11,0,4,0,10,0\n11,0,6,0,8,0,12,0,9,0,7,0\n13,0,9,0,2,0,1,0,"
},
{
"path": "schedules/13_12.csv",
"chars": 672,
"preview": "11,0,3,0,9,0,1,0,12,0,10,0\n8,0,4,0,5,0,2,0,6,0,7,0\n13,0,4,0,7,0,8,0,1,0,11,0\n12,0,6,0,9,0,10,0,5,0,2,0\n12,0,5,0,3,0,13,0"
},
{
"path": "schedules/13_13.csv",
"chars": 749,
"preview": "13,0,9,0,1,0,4,0,7,0,11,0\n12,0,2,0,5,0,6,0,3,0,8,0\n4,0,8,0,9,0,13,0,6,0,10,0\n3,0,7,0,12,0,11,0,2,0,10,0\n5,0,6,0,11,0,8,0"
},
{
"path": "schedules/13_14.csv",
"chars": 802,
"preview": "8,0,12,0,4,0,3,0,10,0,5,0\n9,0,1,0,7,0,13,0,2,0,6,0\n11,0,8,0,4,0,7,0,1,0,13,0\n6,0,5,0,3,0,12,0,9,0,2,0\n11,0,13,0,1,0,5,0,"
},
{
"path": "schedules/13_2.csv",
"chars": 128,
"preview": "11,0,13,0,10,0,12,0,5,0,9,0\n6,0,2,0,4,0,3,0,7,0,8,0\n1,0,3,0,13,0,8,0,10,0,2,0\n9,0,11,0,6,0,7,0,4,0,1,0\n8,1,12,0,3,1,5,0,"
},
{
"path": "schedules/13_3.csv",
"chars": 181,
"preview": "5,0,13,0,12,0,9,0,11,0,1,0\n6,0,2,0,10,0,8,0,7,0,4,0\n3,0,9,0,5,0,13,0,8,0,6,0\n11,0,12,0,2,0,4,0,3,0,10,0\n7,0,6,0,11,0,1,0"
},
{
"path": "schedules/13_4.csv",
"chars": 234,
"preview": "12,0,11,0,13,0,7,0,4,0,1,0\n3,0,8,0,5,0,10,0,9,0,6,0\n9,0,2,0,1,0,12,0,8,0,10,0\n4,0,5,0,2,0,13,0,3,0,7,0\n11,0,1,0,3,0,6,0,"
},
{
"path": "schedules/13_5.csv",
"chars": 284,
"preview": "8,0,2,0,1,0,6,0,3,0,9,0\n13,0,11,0,10,0,4,0,7,0,12,0\n7,0,8,0,6,0,10,0,12,0,5,0\n5,0,13,0,4,0,1,0,9,0,11,0\n3,0,1,0,4,0,2,0,"
},
{
"path": "schedules/13_6.csv",
"chars": 336,
"preview": "4,0,8,0,5,0,6,0,11,0,10,0\n9,0,2,0,12,0,3,0,7,0,1,0\n8,0,13,0,11,0,9,0,1,0,6,0\n12,0,4,0,3,0,2,0,13,0,5,0\n8,0,9,0,10,0,7,0,"
},
{
"path": "schedules/13_7.csv",
"chars": 413,
"preview": "5,0,6,0,2,0,8,0,10,0,11,0\n4,0,12,0,7,0,13,0,9,0,1,0\n9,0,2,0,13,0,3,0,7,0,10,0\n1,0,3,0,4,0,11,0,5,0,8,0\n1,0,8,0,12,0,6,0,"
},
{
"path": "schedules/13_8.csv",
"chars": 465,
"preview": "9,0,4,0,3,0,5,0,12,0,2,0\n10,0,6,0,11,0,7,0,8,0,1,0\n4,0,1,0,6,0,13,0,3,0,2,0\n5,0,7,0,13,0,12,0,9,0,8,0\n12,0,2,0,10,0,11,0"
},
{
"path": "schedules/13_9.csv",
"chars": 518,
"preview": "10,0,1,0,8,0,12,0,2,0,7,0\n5,0,3,0,13,0,9,0,11,0,4,0\n10,0,6,0,2,0,5,0,8,0,4,0\n9,0,11,0,12,0,6,0,7,0,13,0\n6,0,1,0,9,0,3,0,"
},
{
"path": "schedules/14_1.csv",
"chars": 79,
"preview": "9,0,2,0,6,0,5,0,14,0,10,0\n7,0,12,0,4,0,13,0,3,0,8,0\n11,0,13,1,8,1,1,0,10,1,3,1\n"
},
{
"path": "schedules/14_10.csv",
"chars": 628,
"preview": "2,0,1,0,6,0,10,0,14,0,5,0\n12,0,11,0,9,0,7,0,13,0,4,0\n4,0,5,0,8,0,3,0,9,0,7,0\n6,0,10,0,13,0,3,0,11,0,8,0\n12,0,2,0,4,0,14,"
},
{
"path": "schedules/14_11.csv",
"chars": 681,
"preview": "5,0,10,0,14,0,4,0,2,0,1,0\n12,0,9,0,6,0,13,0,7,0,8,0\n3,0,7,0,9,0,11,0,13,0,1,0\n8,0,4,0,6,0,14,0,11,0,3,0\n10,0,2,0,11,0,9,"
},
{
"path": "schedules/14_12.csv",
"chars": 732,
"preview": "8,0,3,0,5,0,12,0,4,0,14,0\n9,0,7,0,10,0,6,0,13,0,11,0\n1,0,3,0,13,0,6,0,2,0,5,0\n2,0,12,0,7,0,8,0,10,0,1,0\n12,0,14,0,9,0,11"
},
{
"path": "schedules/14_13.csv",
"chars": 810,
"preview": "5,0,2,0,1,0,8,0,10,0,13,0\n9,0,6,0,12,0,4,0,3,0,11,0\n10,0,11,0,14,0,6,0,7,0,8,0\n7,0,3,0,12,0,9,0,1,0,4,0\n9,0,13,0,2,0,5,0"
},
{
"path": "schedules/14_14.csv",
"chars": 863,
"preview": "12,0,14,0,9,0,5,0,1,0,6,0\n3,0,10,0,4,0,8,0,13,0,7,0\n10,0,2,0,7,0,1,0,11,0,14,0\n3,0,2,0,12,0,11,0,13,0,9,0\n8,0,5,0,14,0,4"
},
{
"path": "schedules/14_2.csv",
"chars": 131,
"preview": "13,0,6,0,14,0,12,0,9,0,7,0\n2,0,8,0,10,0,3,0,5,0,4,0\n1,0,13,0,2,0,11,0,14,0,8,0\n4,0,11,0,1,0,6,0,10,0,9,0\n5,0,7,0,1,1,11,"
},
{
"path": "schedules/14_3.csv",
"chars": 183,
"preview": "13,0,6,0,5,0,8,0,4,0,3,0\n11,0,12,0,9,0,14,0,2,0,10,0\n7,0,5,0,8,0,1,0,11,0,14,0\n6,0,2,0,12,0,1,0,7,0,13,0\n10,0,4,0,1,0,3,"
},
{
"path": "schedules/14_4.csv",
"chars": 260,
"preview": "1,0,7,0,9,0,3,0,11,0,13,0\n5,0,6,0,4,0,10,0,12,0,8,0\n8,0,14,0,7,0,1,0,2,0,10,0\n2,0,5,0,14,0,6,0,9,0,3,0\n11,0,12,0,6,0,7,0"
},
{
"path": "schedules/14_5.csv",
"chars": 314,
"preview": "5,0,14,0,8,0,3,0,2,0,10,0\n1,0,11,0,13,0,7,0,6,0,4,0\n12,0,4,0,10,0,2,0,9,0,1,0\n3,0,5,0,7,0,6,0,12,0,9,0\n11,0,14,0,9,0,8,0"
},
{
"path": "schedules/14_6.csv",
"chars": 366,
"preview": "7,0,8,0,5,0,3,0,12,0,2,0\n6,0,4,0,1,0,9,0,14,0,13,0\n11,0,9,0,4,0,5,0,10,0,6,0\n14,0,10,0,11,0,13,0,12,0,7,0\n8,0,2,0,10,0,1"
},
{
"path": "schedules/14_7.csv",
"chars": 445,
"preview": "6,0,8,0,11,0,13,0,2,0,14,0\n1,0,5,0,4,0,3,0,10,0,9,0\n12,0,6,0,14,0,2,0,7,0,1,0\n3,0,11,0,7,0,10,0,4,0,13,0\n10,0,12,0,5,0,9"
}
]
// ... and 1421 more files (download for full content)
About this extraction
This page contains the full source code of the Team254/cheesy-arena GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1621 files (3.9 MB), approximately 1.1M tokens, and a symbol index with 1220 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.