Compare commits
No commits in common. "54122c0c3224fcc82556b871aafc52c9f0bcb54b" and "913b9ab6087138777fab17faaba148f2407a657e" have entirely different histories.
54122c0c32
...
913b9ab608
@ -1,104 +0,0 @@
|
||||
<script>
|
||||
import mqtt from "mqtt";
|
||||
|
||||
// Consts
|
||||
let topics = ['display/#', 'division/#'];
|
||||
let states = ["init", "timer", "pre-game", "event-name", "alliance-selection", "break", "red-wins", "blue-wins", "prairies", "rockies", "ab-education", "ab-innovate", "tcenergy", "tourismcalgary", "westmech"];
|
||||
let led_sizes = {
|
||||
"center": { width: 1176, height: 168 },
|
||||
"side": { width: 896, height: 128 }
|
||||
}
|
||||
|
||||
const client = mqtt.connect("ws://10.42.0.36:8883");
|
||||
let displays = {};
|
||||
let division_names = {};
|
||||
let divisions = {};
|
||||
let active_division = "1";
|
||||
|
||||
client.on("connect", () => {
|
||||
console.log("connected to mqtt");
|
||||
|
||||
topics.forEach((topic) => {
|
||||
client.subscribe(topic, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
} else {
|
||||
console.log(`subscribed to topic '${topic}'`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
client.on("message", (topic, message) => {
|
||||
let display_field_match = /^display\/(\d+)\/(\d+)\/(\w+)$/.exec(topic);
|
||||
let division_metadata_match = /^division\/(\d+)\/(\w+)$/.exec(topic);
|
||||
let division_stat_match = /^division\/(\d+)\/(\w+)\/(\d+)(?:\/(\w+))?$/.exec(topic);
|
||||
|
||||
if(division_metadata_match) {
|
||||
let [_, division, type] = division_metadata_match;
|
||||
|
||||
switch(type) {
|
||||
case "name":
|
||||
division_names[division] = message.toString();
|
||||
console.log(`Set division ${type} for ${division} to ${message.toString()}`)
|
||||
console.log(divisions);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown division metadata type: ${type}`);
|
||||
}
|
||||
} else if(division_stat_match) {
|
||||
let [_, division, matchType, matchNumber, type] = division_stat_match;
|
||||
type ||= "info";
|
||||
|
||||
divisions[division] ||= {};
|
||||
divisions[division][matchType] ||= {};
|
||||
divisions[division][matchType][matchNumber] ||= {};
|
||||
|
||||
divisions[division][matchType][matchNumber][type] ||= {};
|
||||
} else if(display_field_match) {
|
||||
let [_, division, field, type] = display_field_match;
|
||||
|
||||
displays[division] ||= {};
|
||||
displays[division][field] ||= {};
|
||||
displays[division][field][type] = message.toString();
|
||||
console.log(displays[active_division]);
|
||||
} else {
|
||||
console.warn(`no match for topic ${topic}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="displays">
|
||||
<select bind:value={active_division} on:change={() => console.log(`Changed: ${active_division}`)}>
|
||||
{#each Object.entries(division_names) as [division, name]}
|
||||
<option value={division} selected={active_division === division}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{#if division_names[active_division] === undefined}
|
||||
<p>Division {active_division} undefined</p>
|
||||
{:else}
|
||||
<div class="division">
|
||||
<h2>{`${division_names[active_division]} Division`}</h2>
|
||||
{#each Object.entries(displays[active_division]) as [field, field_data] }
|
||||
<div class="field">
|
||||
<h3>{field} - {field_data.name}</h3>
|
||||
<iframe src="/?field={active_division}/{field}" width={led_sizes[field_data.class].width} height={led_sizes[field_data.class].height} title="Field {active_division}/{field}"></iframe>
|
||||
<br />
|
||||
<select bind:value={field_data.class} on:change={() => client.publish(`display/${active_division}/${field}/class`, field_data.class, { retain: true })}>
|
||||
{#each Object.entries(led_sizes) as [size, _]}
|
||||
<option value={size}>{size}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select bind:value={field_data.state} on:change={() => client.publish(`display/${active_division}/${field}/state`, field_data.state, { retain: true })}>
|
||||
{#each states as s}
|
||||
<option value={s}>
|
||||
{s}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
@ -1,227 +0,0 @@
|
||||
<script>
|
||||
import mqtt from "mqtt";
|
||||
|
||||
let div_id;
|
||||
|
||||
|
||||
/** @type {string[]} */
|
||||
|
||||
let red_teams = [];
|
||||
let blue_teams = [];
|
||||
let field_matches = [];
|
||||
|
||||
|
||||
let latest_start_time = 0;
|
||||
let active_field = 0;
|
||||
let offset = 0;
|
||||
|
||||
|
||||
let event_name = "Mecha Mayhem 2024";
|
||||
let match_name = "No Match";
|
||||
let match_name_show = "No Match";
|
||||
/** @type {string[]} */
|
||||
let teams_red = [];
|
||||
/** @type {string[]} */
|
||||
let teams_blue = [];
|
||||
|
||||
/** @type {undefined | Date} */
|
||||
let timer_end = undefined;
|
||||
/** @type {undefined | ReturnType<typeof setInterval>} */
|
||||
let timer_next_tick = undefined;
|
||||
let timer = "Next Match";
|
||||
|
||||
/** @type {string} */
|
||||
let display_state = "init";
|
||||
|
||||
/** @type {string} */
|
||||
let display_class = "side-display";
|
||||
|
||||
const client = mqtt.connect("ws://metznet.ca:8883");
|
||||
|
||||
function stop_timer() {
|
||||
if(timer_next_tick === undefined) {
|
||||
} else {
|
||||
clearInterval(timer_next_tick);
|
||||
timer_next_tick = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function tick_timer() {
|
||||
if(timer_end === undefined) {
|
||||
stop_timer();
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const time_diff = timer_end.getTime() - now.getTime() + offset;
|
||||
if (time_diff <= 0) {
|
||||
timer = "0:00";
|
||||
stop_timer();
|
||||
} else {
|
||||
const seconds_total = Math.ceil(time_diff/ 1000);
|
||||
const minutes = Math.floor(seconds_total / 60);
|
||||
const seconds = seconds_total - (minutes * 60);
|
||||
timer = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
client.on("message", (topic, message) => {
|
||||
/** @type {RegExp} */
|
||||
let field_metadata_topic = /^field\/\d+\/(\d+)$/;
|
||||
/** @type {RegExp} */
|
||||
let field_state_topic = /^field\/\d+\/(\d+)\/state$/;
|
||||
|
||||
let topic_str = topic.toString();
|
||||
let message_str = message.toString();
|
||||
let field_state_match = field_state_topic.exec(topic_str);
|
||||
let field_metadata_match = field_metadata_topic.exec(topic_str);
|
||||
|
||||
console.log(`${topic_str} - ${message_str}`);
|
||||
|
||||
if (field_state_match) {
|
||||
console.log("here")
|
||||
let [_, field] = field_state_match;
|
||||
const state_obj = JSON.parse(message_str);
|
||||
if (state_obj.state != "Scheduled") { return; }
|
||||
if (state_obj.start <= latest_start_time) { return; }
|
||||
|
||||
latest_start_time = parseFloat(state_obj.start);
|
||||
active_field = parseInt(field);
|
||||
console.log(`${latest_start_time}`);
|
||||
console.log(`active field: ${active_field}`);
|
||||
|
||||
let field_obj = field_matches[active_field];
|
||||
red_teams = [field_obj.red_teams[0], field_obj.red_teams[1]];
|
||||
blue_teams = [field_obj.blue_teams[0], field_obj.blue_teams[1]];
|
||||
}
|
||||
else if (field_metadata_match) {
|
||||
let [_, field] = field_metadata_match;
|
||||
const field_obj = JSON.parse(message_str);
|
||||
|
||||
field_matches[parseInt(field)] = field_obj;
|
||||
if(field == active_field) {
|
||||
red_teams = [field_obj.red_teams[0], field_obj.red_teams[1]];
|
||||
blue_teams = [field_obj.blue_teams[0], field_obj.blue_teams[1]];
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(`${topic_str} not defined`);
|
||||
}
|
||||
|
||||
|
||||
// switch(topic_str){
|
||||
// case "time":
|
||||
// let local = Date.now();
|
||||
// let server = JSON.parse(message_str) * 1000;
|
||||
// offset = local - server;
|
||||
// console.log(`NEW offset: ${local} - ${server} = ${offset}`);
|
||||
// break;
|
||||
// case field_metadata_topic:
|
||||
// const field_obj = JSON.parse(message_str);
|
||||
// teams_red = [field_obj.red_teams[0], field_obj.red_teams[1]];
|
||||
// teams_blue = [field_obj.blue_teams[0], field_obj.blue_teams[1]];
|
||||
// let num = field_obj.tuple.match_num;
|
||||
// let round = field_obj.tuple.round;
|
||||
// match_name = `${round} ${num}`;
|
||||
// break;
|
||||
// case field_state_topic:
|
||||
// stop_timer();
|
||||
|
||||
// const state_obj = JSON.parse(message_str);
|
||||
// const start_ms = state_obj.start * 1000;
|
||||
// console.log(`Start_ms: ${start_ms}`);
|
||||
// switch(state_obj.state) {
|
||||
// case "Scheduled":
|
||||
// timer = match_name;
|
||||
// match_name_show = "Next Match";
|
||||
// if (state_obj.start > latest_start_time) {
|
||||
// latest_start_time = state_obj.start;
|
||||
// active_field = parseFloat(topic_str.split("/")[2]);
|
||||
// console.log(`${latest_start_time}`);
|
||||
// console.log(`${active_field}`);
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// break;
|
||||
// default:
|
||||
// console.log(`Unhandled topic ${topic_str}`)
|
||||
// }
|
||||
});
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
onMount(() => {
|
||||
const search_params = new URLSearchParams(window.location.search);
|
||||
div_id = search_params.get("division");
|
||||
if (div_id === null) {
|
||||
div_id = "default";
|
||||
}
|
||||
// field_metadata_topic = `field/${div_id}/+`;
|
||||
// field_state_topic = `field/${div_id}/+/state`;
|
||||
|
||||
client.on("connect", () => {
|
||||
console.log("connected to mqtt");
|
||||
|
||||
client.subscribe(`field/${div_id}/#`, (err) => {
|
||||
if (err) {
|
||||
console.log("failed to subscribe to field metadata")
|
||||
console.log(err);
|
||||
} else {
|
||||
console.log(`subscribed`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div class="img-grid">
|
||||
{#each Object.entries(red_teams) as [index, team]}
|
||||
<div class="item">
|
||||
<video autoplay loop muted playsinline src="./{team}.mp4" alt="video {team}"></video>
|
||||
<div class="text">{team}</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#each Object.entries(blue_teams) as [index, team]}
|
||||
<div class="item">
|
||||
<video autoplay loop muted playsinline src="./{team}.mp4" alt="video {team}"></video>
|
||||
<div class="text">{team}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap');
|
||||
|
||||
.img-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, lfr);
|
||||
grid-template-rows: repeat(2, lfr);
|
||||
gap: 20px;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
margin: auto;
|
||||
background-color: #2ae400;
|
||||
font-family: 'Bebas Neue';
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 2.6 MiB |
After Width: | Height: | Size: 820 KiB |
After Width: | Height: | Size: 65 MiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 25 MiB |
After Width: | Height: | Size: 739 KiB |
After Width: | Height: | Size: 25 MiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 24 MiB |
After Width: | Height: | Size: 3.2 MiB |