other/train-to-cloud-city/devices/rfid/trainGame.js (135 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const {
clearTrainMailbox,
trainMailboxListener,
signalListener,
proposalListener,
updateInputMailbox,
submitActualCargo,
} = require("./utils/firestoreHelpers.js");
const { getMotor } = require("./utils/train.js");
const { queueMessageToPublish } = require("./utils/metrics.js");
// Cargo reading
let beginReading = false;
let stockedCargo = [];
// Cargo results & Mailbox
let trainMailbox = {};
let proposalResult = {};
// Train movement states
let moveBackToStation = false;
let moveForwardsToStation = false;
let signalLights = {};
// Hardcoded rfid tag id to capture front car
// and back car (to calculate middle cargo)
const frontCar = "\x03\x02330035AD1EB5\r";
const backCar = "\x03\x023300348E9019\r";
/**
* Train mailbox listener
*/
trainMailboxListener(async (snapshot) => {
trainMailbox = snapshot?.data() || {};
trainMailbox && (await updateGameLoop());
});
/**
* Signal listener
* ----------------------
*/
signalListener(async (snapshot) => {
const { one, two, three, four } = snapshot?.data() || {};
signalLights?.["signal_1"]?.(one?.target_state || "off");
signalLights?.["signal_2"]?.(two?.target_state || "off");
signalLights?.["signal_3"]?.(three?.target_state || "off");
signalLights?.["signal_4"]?.(four?.target_state || "off");
});
/**
* Proposal listener
* ----------------------
*/
proposalListener(async (snapshot) => {
proposalResult = snapshot?.data()?.proposal_result || {};
if (proposalResult) {
const motor = await getMotor();
if (trainMailbox?.input === "do_check_cargo") {
// If cargo isn't valid, head back to station to reload cargo
if (proposalResult?.reason && !proposalResult?.clear) {
moveBackToStation = true;
motor?.setPower(-30);
queueMessageToPublish("cargo-reload", { stockedCargo });
stockedCargo = [];
}
}
}
});
/**
* readCargo
* ----------------------
* We're at station at this point
*/
async function readCargo(chunk, role) {
const motor = await getMotor();
// In either cargo error & reload stage (backwards to station)
// Or in victory lap mode (forwards to station)
if (moveBackToStation || moveForwardsToStation) {
await moveToStation(chunk, role);
return;
}
const tagId = new String(chunk);
const isFrontCar = frontCar.includes(tagId);
const isBackCar = backCar.includes(tagId);
const isCargo = !isFrontCar && !isBackCar;
// MIDDLE: In the middle of train, store cargo chunk and continue on
if (isCargo && beginReading) {
stockedCargo.push(chunk);
}
// FRONT: Begin reading cargo
if (isFrontCar) {
beginReading = true;
}
// BACK: At tailend of train, wrap up and send read cargo to firestore
if (isBackCar) {
beginReading = false;
motor?.brake();
motor?.stop();
try {
// Submit held cargo
await submitActualCargo(stockedCargo);
queueMessageToPublish("cargo-read", { stockedCargo });
} catch (error) {
console.error(error);
}
}
}
/**
* moveToStation
* ----------------------
* In either cargo error & reload stage (backwards to station)
* Or in victory lap mode (forwards to station)
*/
async function moveToStation(chunk, role) {
const tagId = new String(chunk);
const motor = await getMotor();
const isFrontCar = frontCar.includes(tagId);
if (isFrontCar && role === "station") {
motor?.brake();
motor?.stop();
moveBackToStation = false;
moveForwardsToStation = false;
return;
}
moveForwardsToStation && motor?.setPower(25);
moveBackToStation && motor?.setPower(-40);
}
/**
* updateGameLoop
* ----------------------
* Main game loop for train. Callback fn to all
* serialport / rfid readers so state is not held within loop
*/
async function updateGameLoop() {
const motor = await getMotor();
motor?.brake();
motor?.stop();
if (trainMailbox?.input === "do_check_cargo") {
motor?.setPower(25);
return;
}
if (trainMailbox?.input === "do_victory_lap") {
if (moveForwardsToStation) {
// Victory lap completed, now reset
moveForwardsToStation = false;
motor?.stop();
// Reset train mailbox
resetGameState();
await clearTrainMailbox();
console.log("Session success!");
await updateInputMailbox("reset");
} else {
// Go on victory lap
moveForwardsToStation = true;
motor?.setPower(40);
queueMessageToPublish("victory", {});
}
}
}
/**
* changeSignalLight
* ----------------------
* "this" refers to opened serial
* port & sends in one of three states
* to Adafruit QTPy RP2040 signal.
* ----------------------
* clear (green led) - "cleared" to proceed
* stop (red led)
* off (led should be off)
*
*/
function changeSignalLight(state) {
if (state === "clear" || state === "stop" || state === "off") {
this?.write(`${state}\n`);
}
}
/**
* storeSignal
* ----------------------
* Stores function to change light states
* that is binded to serial port reference
*/
async function storeSignal(role = "", listener = () => {}) {
if (role !== "") {
signalLights[role] = changeSignalLight.bind(listener);
}
}
/**
* resetGameState
* ----------------------
* Clears & resets global variables
* and any other game states
*/
async function resetGameState() {
signalLights = {};
signalLightsUpdate = {};
// Reset cargo reading
beginReading = false;
stockedCargo = [];
// Reset cargo results & Mailbox
trainMailbox = {};
proposalResult = {};
// Reset train movement states
moveBackToStation = false;
moveForwardsToStation = false;
}
module.exports = {
readCargo,
storeSignal,
updateGameLoop,
resetGameState,
};