diff --git a/src/main.rs b/src/main.rs index 48a53f0..e7a8dbe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use log::warn; use rumqttc::{MqttOptions, Client, QoS, LastWill}; use bytes::Bytes; use std::time::Duration; @@ -19,23 +20,23 @@ use std::sync::mpsc; // MQTT Topics: // - division/{division_id}/{round}/{match}/score // - division/{division_id}/ranking -// - arena/{arena_id}/score -// - arena/{arena_id}/state -// - arena/{arena_id} +// - field/{fieldset_id}/{field_id}/score +// - field/{fieldset_id}/{field_id}/state +// - field/{fieldset_id}/{field_id} // - team/{team_string} pub mod tm { include!(concat!(env!("OUT_DIR"), "/tm.rs")); } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] enum GameSide { Red, Blue, Tie, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] struct AllianceScore { auton_wp: bool, team_goal: i32, @@ -45,8 +46,8 @@ struct AllianceScore { elevation_tiers: [i32; 2], } -#[derive(Serialize, Deserialize, Debug)] -struct GameScore { +#[derive(Serialize, Deserialize, Debug, Clone)] +struct MatchScore { autonomous_winner: Option, red_score: AllianceScore, red_total: i32, @@ -157,11 +158,10 @@ impl Event { // TODO: remove extra entries instead of just adding new ones fn parse_field_sets(self: &mut Event, sets: tm::FieldSetList) { for set in sets.field_sets { - let mut fields = Vec::new(); + let mut fields = HashMap::new(); for field in &set.fields { - fields.push(Field{ + fields.insert(field.id(), Field{ name: String::from(field.name()), - id: field.id(), last_known_match: None, }); } @@ -196,6 +196,7 @@ impl Event { match_list.push(Match{ state: None, info: None, + score: None, tuple: tuple.clone(), }) }, @@ -204,6 +205,7 @@ impl Event { new_match_list.push(Match{ state: None, info: None, + score: None, tuple: tuple.clone(), }); matches.insert(tuple.division, new_match_list); @@ -221,7 +223,7 @@ impl Event { #[derive(Serialize, Deserialize, Debug, Clone)] struct DivisionState { - current_arena: u32, + current_field: FieldTuple, current_match: MatchTuple, } @@ -234,21 +236,19 @@ struct Division { #[derive(Serialize, Deserialize, Debug, Clone)] struct FieldSet { - fields: Vec, + fields: HashMap, } #[derive(Serialize, Deserialize, Debug, Clone)] struct Field { name: String, - id: i32, last_known_match: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] struct MatchState { - state: Option, - start_s: Option, - start_ns: Option, + state: GameState, + start: f64, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -261,6 +261,7 @@ struct MatchInfo { struct Match { state: Option, info: Option, + score: Option, tuple: MatchTuple, } @@ -620,6 +621,7 @@ struct StateChange { field: FieldTuple, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash)] struct FieldTuple { set: i32, id: i32, @@ -672,165 +674,157 @@ fn get_affected_match(notice: &tm::Notice) -> Option { } } -fn get_game_score(notice: &tm::Notice) -> Option { - match ¬ice.match_score { - None => None, - Some(scores) => { - if scores.alliances.len() != 2 { - return None; - } - - let ref red_score = scores.alliances[0]; - let ref blue_score = scores.alliances[1]; - - let mut out = GameScore{ - autonomous_winner: None, - red_total: 0, - blue_total: 0, - blue_score: AllianceScore{ - auton_wp: false, - team_goal: 0, - team_zone: 0, - green_goal: 0, - green_zone: 0, - elevation_tiers: [0, 0], - }, - red_score : AllianceScore{ - auton_wp: false, - team_goal: 0, - team_zone: 0, - green_goal: 0, - green_zone: 0, - elevation_tiers: [0, 0], - }, - }; +fn get_game_score(scores: &tm::MatchScore) -> Option { + if scores.alliances.len() != 2 { + return None; + } - for symbol in red_score.score_types.iter() { - match symbol.name.as_str() { - "auto" => if symbol.val != 0 {out.autonomous_winner = Some(GameSide::Red)}, - "auto_tie" => if symbol.val != 0 {out.autonomous_winner = Some(GameSide::Tie)}, - "auto_wp" => out.red_score.auton_wp = symbol.val != 0, - "zone_alliance_triballs" => out.red_score.team_zone = symbol.val, - "goal_alliance_triballs" => out.red_score.team_goal = symbol.val, - "goal_triballs" => out.red_score.green_goal = symbol.val, - "zone_triballs" => out.red_score.green_zone = symbol.val, - "elevation_tier_1" => out.red_score.elevation_tiers[0] = symbol.val, - "elevation_tier_2" => out.red_score.elevation_tiers[1] = symbol.val, - _ => {}, - } - } + let ref red_score = scores.alliances[0]; + let ref blue_score = scores.alliances[1]; + + let mut out = MatchScore{ + autonomous_winner: None, + red_total: 0, + blue_total: 0, + blue_score: AllianceScore{ + auton_wp: false, + team_goal: 0, + team_zone: 0, + green_goal: 0, + green_zone: 0, + elevation_tiers: [0, 0], + }, + red_score : AllianceScore{ + auton_wp: false, + team_goal: 0, + team_zone: 0, + green_goal: 0, + green_zone: 0, + elevation_tiers: [0, 0], + }, + }; + + for symbol in red_score.score_types.iter() { + match symbol.name.as_str() { + "auto" => if symbol.val != 0 {out.autonomous_winner = Some(GameSide::Red)}, + "auto_tie" => if symbol.val != 0 {out.autonomous_winner = Some(GameSide::Tie)}, + "auto_wp" => out.red_score.auton_wp = symbol.val != 0, + "zone_alliance_triballs" => out.red_score.team_zone = symbol.val, + "goal_alliance_triballs" => out.red_score.team_goal = symbol.val, + "goal_triballs" => out.red_score.green_goal = symbol.val, + "zone_triballs" => out.red_score.green_zone = symbol.val, + "elevation_tier_1" => out.red_score.elevation_tiers[0] = symbol.val, + "elevation_tier_2" => out.red_score.elevation_tiers[1] = symbol.val, + _ => {}, + } + } - for symbol in blue_score.score_types.iter() { - match symbol.name.as_str() { - "auto" => if symbol.val != 0 {out.autonomous_winner = Some(GameSide::Blue)}, - "auto_tie" => if symbol.val != 0 {out.autonomous_winner = Some(GameSide::Tie)}, - "auto_wp" => out.blue_score.auton_wp = symbol.val != 0, - "zone_alliance_triballs" => out.blue_score.team_zone = symbol.val, - "goal_alliance_triballs" => out.blue_score.team_goal = symbol.val, - "goal_triballs" => out.blue_score.green_goal = symbol.val, - "zone_triballs" => out.blue_score.green_zone = symbol.val, - "elevation_tier_1" => out.blue_score.elevation_tiers[0] = symbol.val, - "elevation_tier_2" => out.blue_score.elevation_tiers[1] = symbol.val, - _ => {}, - } - } + for symbol in blue_score.score_types.iter() { + match symbol.name.as_str() { + "auto" => if symbol.val != 0 {out.autonomous_winner = Some(GameSide::Blue)}, + "auto_tie" => if symbol.val != 0 {out.autonomous_winner = Some(GameSide::Tie)}, + "auto_wp" => out.blue_score.auton_wp = symbol.val != 0, + "zone_alliance_triballs" => out.blue_score.team_zone = symbol.val, + "goal_alliance_triballs" => out.blue_score.team_goal = symbol.val, + "goal_triballs" => out.blue_score.green_goal = symbol.val, + "zone_triballs" => out.blue_score.green_zone = symbol.val, + "elevation_tier_1" => out.blue_score.elevation_tiers[0] = symbol.val, + "elevation_tier_2" => out.blue_score.elevation_tiers[1] = symbol.val, + _ => {}, + } + } - match &out.autonomous_winner { - None => {}, - Some(winner) => match winner { - GameSide::Red => {out.red_total = 8; out.blue_total = 0;}, - GameSide::Blue => {out.red_total = 0; out.blue_total = 8;}, - GameSide::Tie => {out.red_total = 4; out.blue_total = 4;}, - }, - } + match &out.autonomous_winner { + None => {}, + Some(winner) => match winner { + GameSide::Red => {out.red_total = 8; out.blue_total = 0;}, + GameSide::Blue => {out.red_total = 0; out.blue_total = 8;}, + GameSide::Tie => {out.red_total = 4; out.blue_total = 4;}, + }, + } - out.red_total += 5 * (out.red_score.green_goal + out.red_score.team_goal); - out.red_total += 2 * (out.red_score.green_zone + out.red_score.team_zone); + out.red_total += 5 * (out.red_score.green_goal + out.red_score.team_goal); + out.red_total += 2 * (out.red_score.green_zone + out.red_score.team_zone); - out.blue_total += 5 * (out.blue_score.green_goal + out.blue_score.team_goal); - out.blue_total += 2 * (out.blue_score.green_zone + out.blue_score.team_zone); + out.blue_total += 5 * (out.blue_score.green_goal + out.blue_score.team_goal); + out.blue_total += 2 * (out.blue_score.green_zone + out.blue_score.team_zone); - let mut elevations = [(0, out.red_score.elevation_tiers[0]), (1, out.red_score.elevation_tiers[1]), (2, out.blue_score.elevation_tiers[0]), (3, out.blue_score.elevation_tiers[1])]; - elevations.sort_by(|a, b| b.1.cmp(&a.1)); - let mut elev_list = Vec::new(); - let mut elev_map: HashMap> = HashMap::new(); + let mut elevations = [(0, out.red_score.elevation_tiers[0]), (1, out.red_score.elevation_tiers[1]), (2, out.blue_score.elevation_tiers[0]), (3, out.blue_score.elevation_tiers[1])]; + elevations.sort_by(|a, b| b.1.cmp(&a.1)); + let mut elev_list = Vec::new(); + let mut elev_map: HashMap> = HashMap::new(); - for elevation in elevations { - match elev_list.last() { - None => elev_list.push(elevation.1), - Some(last) => if *last != elevation.1 {elev_list.push(elevation.1)}, - } + for elevation in elevations { + match elev_list.last() { + None => elev_list.push(elevation.1), + Some(last) => if *last != elevation.1 {elev_list.push(elevation.1)}, + } - match elev_map.get_mut(&elevation.1) { - None => { - let mut holders = Vec::new(); - holders.push(elevation.0); - elev_map.insert(elevation.1, holders); - }, - Some(cur) => cur.push(elevation.0), - } - } + match elev_map.get_mut(&elevation.1) { + None => { + let mut holders = Vec::new(); + holders.push(elevation.0); + elev_map.insert(elevation.1, holders); + }, + Some(cur) => cur.push(elevation.0), + } + } - for (idx, elevation) in elev_list.iter().enumerate() { - if *elevation == 0i32 { - break; - } - match elev_map.get(&elevation) { - None => {}, - Some(holders) => { - for holder in holders { - match holder { - 0 => out.red_total += 20 - (5*idx as i32), - 1 => out.red_total += 20 - (5*idx as i32), - 2 => out.blue_total += 20 - (5*idx as i32), - 3 => out.blue_total += 20 - (5*idx as i32), - _ => {}, - } - } + for (idx, elevation) in elev_list.iter().enumerate() { + if *elevation == 0i32 { + break; + } + match elev_map.get(&elevation) { + None => {}, + Some(holders) => { + for holder in holders { + match holder { + 0 => out.red_total += 20 - (5*idx as i32), + 1 => out.red_total += 20 - (5*idx as i32), + 2 => out.blue_total += 20 - (5*idx as i32), + 3 => out.blue_total += 20 - (5*idx as i32), + _ => {}, } } } - - return Some(out); - }, + } } + return Some(out); } fn on_score_change(notice: tm::Notice, event: &mut Event, _connection: &mut TMConnection) -> Vec { - match get_affected_match(¬ice) { - None => Vec::new(), - Some(tuple) => { - match get_game_score(¬ice) { - None => Vec::new(), - Some(score) => { - let serialized = serde_json::to_string(&score).unwrap(); - let game_topic = format!("division/{}/{:?}/{}/score", tuple.division, tuple.round, tuple.match_num); - let mut out = Vec::new(); - out.push(MQTTMessage{ - topic: game_topic, - payload: serialized.clone(), - }); - for (_, field_set) in &event.field_sets { - for field in &field_set.fields { - match field.last_known_match { - None => {}, - Some(last_known_match) => { - if last_known_match == tuple { - let field_topic = format!("field/{}/score", field.id); - out.push(MQTTMessage{ - topic: field_topic, - payload: serialized.clone(), - }); - } - }, - } - } + let Some(tuple) = get_affected_match(¬ice) else { return Vec::new() }; + let Some(scores) = notice.match_score else { return Vec::new() }; + let Some(score) = get_game_score(&scores) else { return Vec::new() }; + let Some(division) = &mut event.divisions.get_mut(&tuple.division) else { return Vec::new() }; + let Some(m) = &mut division.matches.iter_mut().find(|a| a.tuple == tuple) else { return Vec::new() }; + + m.score = Some(score.clone()); + + let serialized = serde_json::to_string_pretty(&score).unwrap(); + let game_topic = format!("division/{}/{:?}/{}/score", tuple.division, tuple.round, tuple.match_num); + let mut out = Vec::new(); + out.push(MQTTMessage{ + topic: game_topic, + payload: serialized.clone(), + }); + for (field_set_id, field_set) in &event.field_sets { + for (field_id, field) in &field_set.fields { + match field.last_known_match { + None => {}, + Some(last_known_match) => { + if last_known_match == tuple { + let field_topic = format!("field/{}/{}/score", field_set_id, field_id); + out.push(MQTTMessage{ + topic: field_topic, + payload: serialized.clone(), + }); } - return out; }, } - }, + } } + return out; } fn on_match_list_update(_notice: tm::Notice, event: &mut Event, connection: &mut TMConnection) -> Vec { @@ -843,7 +837,7 @@ fn on_match_list_update(_notice: tm::Notice, event: &mut Event, connection: &mut for (division_id, division) in &event.divisions { messages.push(MQTTMessage{ topic: format!("division/{}/schedule", division_id), - payload: serde_json::to_string(&division.matches).unwrap(), + payload: serde_json::to_string_pretty(&division.matches).unwrap(), }); } }, @@ -851,18 +845,84 @@ fn on_match_list_update(_notice: tm::Notice, event: &mut Event, connection: &mut return messages; } -fn on_timer_start(notice: tm::Notice, _event: &mut Event, _connection: &mut TMConnection) -> Vec { - println!("State: {:#?}", notice.field_time.unwrap()); +fn on_field_assigned(notice: tm::Notice, event: &mut Event, connection: &mut TMConnection) -> Vec { + let Some(field_info) = ¬ice.field else { return Vec::new() }; + let Some(tuple) = get_affected_match(¬ice) else { return Vec::new() }; + let Some(field_set) = &mut event.field_sets.get_mut(&field_info.field_set_id()) else { return Vec::new() }; + let Some(field) = &mut field_set.fields.get_mut(&field_info.id()) else { return Vec::new() }; + let Some(division) = &event.divisions.get(&tuple.division) else { return Vec::new() }; + let Some(m) = &division.matches.iter().find(|a| a.tuple == tuple) else { return Vec::new() }; + + field.last_known_match = Some(tuple); + let mut messages = Vec::new(); + if let Some(score) = &m.score { + let serialized = serde_json::to_string_pretty(&score).unwrap(); + let topic = format!("field/{}/{}/score", &field_info.field_set_id(), &field_info.id()); + messages.push(MQTTMessage{ + topic, + payload: serialized, + }); + } + + if let Some(state) = &m.state { + let serialized = serde_json::to_string_pretty(&state).unwrap(); + let topic = format!("field/{}/{}/state", &field_info.field_set_id(), &field_info.id()); + messages.push(MQTTMessage{ + topic, + payload: serialized, + }); + } + return messages; +} + +fn on_timer_start(notice: tm::Notice, event: &mut Event, connection: &mut TMConnection) -> Vec { // 1) Find the state associated with the current block(driver or auton) - // 2) get the match tuple from the arena - // 3) add the mqtt messages for match & arena states + // 2) get the match tuple from the field + // 3) add the mqtt messages for match & field states // 4) queue the state change to ${state}_done // 5) add the cancel_send to the tmconnections map of timeout_sends - return Vec::new(); + let Some(field_time) = notice.field_time else { return Vec::new() }; + let Some(field_info) = &field_time.field else { return Vec::new() }; + let Some(block_list) = &field_time.block_list else { return Vec::new() }; + let Some(current_block_idx) = &field_time.current_block else { return Vec::new() }; + let Some(current_block_start) = &field_time.current_block_start else { return Vec::new() }; + let Some(current_block_end) = &field_time.current_block_end else { return Vec::new() }; + let Some(field_set) = &event.field_sets.get(&field_info.field_set_id()) else { return Vec::new() }; + let Some(field) = &field_set.fields.get(&field_info.id()) else { return Vec::new() }; + let Some(tuple) = &field.last_known_match else { return Vec::new() }; + let Some(division) = &mut event.divisions.get_mut(&tuple.division) else { return Vec::new() }; + let Some(m) = &mut division.matches.iter_mut().find(|a| a.tuple == *tuple) else { return Vec::new() }; + + let current_block = &block_list.entries[*current_block_idx as usize]; + + let messages = Vec::new(); + + if current_block.r#type == Some(2) { //Auto + + } else if current_block.r#type == Some(3) { //Driver + m.state = Some(MatchState{ + state: GameState::Driver, + start: *current_block_start, + }); + let field_tuple = FieldTuple{ + set: field_info.field_set_id(), + id: field_info.id(), + }; + let cancel_state = connection.queue_state_change(Duration::from_secs(current_block.seconds() as u64), StateChange{ + next_state: MatchState{ + state: GameState::DriverDone, + start: *current_block_end, + }, + tuple: *tuple, + field: field_tuple.clone(), + }); + connection.state_cancels.insert(field_tuple, cancel_state); + } + + return messages; } enum Work { - Exit, Notice(Box), State(StateChange), } @@ -875,6 +935,7 @@ fn main() { callbacks.insert(tm::NoticeId::NoticeMatchScoreUpdated, on_score_change); callbacks.insert(tm::NoticeId::NoticeMatchListUpdated, on_match_list_update); callbacks.insert(tm::NoticeId::NoticeFieldTimerStarted, on_timer_start); + callbacks.insert(tm::NoticeId::NoticeFieldMatchAssigned, on_field_assigned); let mut mqttoptions = MqttOptions::new("vex-bridge", "localhost", 1883); mqttoptions.set_keep_alive(Duration::from_secs(5)); @@ -917,12 +978,34 @@ fn main() { let match_list_resp = tm_connection.request(1002, tm::BackendMessageData::default()); event.parse_match_list(match_list_resp.data.match_list.unwrap()); + for (_, division) in &mut event.divisions { + for m in &mut division.matches { + let mut req = tm::BackendMessageData::default(); + let mut filter = tm::MatchTuple::default(); + filter.division = Some(m.tuple.division); + filter.r#match = Some(m.tuple.match_num); + filter.instance = Some(m.tuple.instance); + filter.session = Some(m.tuple.session); + filter.round = Some(m.tuple.round as i32); + req.match_tuple = Some(filter); + let resp = tm_connection.request(1000, req); + match resp.data.match_score { + None => {}, + Some(scores) => { + let score = get_game_score(&scores); + m.score = score.clone(); + client.publish(format!("division/{}/{:?}/{}", m.tuple.division, m.tuple.round, m.tuple.match_num), QoS::AtLeastOnce, true, serde_json::to_string_pretty(&score).unwrap()).expect("MQTT publish fail"); + }, + } + } + } + for (field_set_id, field_set) in &event.field_sets { - for field in &field_set.fields { + for (field_id, field) in &field_set.fields { let mut field_req = tm::BackendMessageData::default(); let mut field_data = tm::OnFieldMatch::default(); let mut f = tm::Field::default(); - f.id = Some(field.id); + f.id = Some(*field_id); f.field_set_id = Some(*field_set_id); field_data.field = Some(f); field_req.on_field_match = Some(field_data); @@ -931,10 +1014,11 @@ fn main() { } } + println!("Event: {:#?}", event); + while running { match tm_connection.work_queue.recv() { Ok(work) => match work { - Work::Exit => running = false, Work::Notice(notice) => { let callback = callbacks.get(¬ice.id()); match callback { @@ -947,7 +1031,7 @@ fn main() { Some(callback) => { let messages = callback(*notice, &mut event, &mut tm_connection); for message in messages { - let result = client.publish(message.topic, QoS::AtMostOnce, true, message.payload); + let result = client.publish(message.topic, QoS::AtLeastOnce, true, message.payload); match result { Ok(_) => {}, Err(error) => log::error!("Publish error: {}", error), @@ -960,7 +1044,12 @@ fn main() { match event.get_match(state_change.tuple) { None => log::warn!("Received state change for unknown match {:#?}", state_change.tuple), Some(m) => { - m.state = Some(state_change.next_state); + m.state = Some(state_change.next_state.clone()); + let field_state_topic = format!("field/{}/{}/state", state_change.field.set, state_change.field.id); + let match_state_topic = format!("division/{}/{:?}/{}/score", state_change.tuple.division, state_change.tuple.round, state_change.tuple.match_num); + let payload = serde_json::to_vec_pretty(&state_change.next_state).unwrap(); + client.publish(field_state_topic, QoS::AtLeastOnce, true, payload.clone()).expect("Failed MQTT publish"); + client.publish(match_state_topic, QoS::AtLeastOnce, true, payload).expect("Failed MQTT publish"); }, } },