diff --git a/build.gradle b/build.gradle index 2c5a21b..fc18d7e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id "java" - id "edu.wpi.first.GradleRIO" version "2025.3.1" + id "edu.wpi.first.GradleRIO" version "2025.3.2" id "idea" } @@ -72,6 +72,8 @@ dependencies { implementation 'org.projectlombok:lombok:1.18.36' annotationProcessor 'org.projectlombok:lombok:1.18.36' + implementation 'com.google.code.gson:gson:2.11.0' + testImplementation platform("org.junit:junit-bom:5.10.1") testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" diff --git a/ctre_sim/CANCoder vers. H - 012 - 0 - ext.dat b/ctre_sim/CANCoder vers. H - 012 - 0 - ext.dat index b5bbc9a..e5e60cc 100644 Binary files a/ctre_sim/CANCoder vers. H - 012 - 0 - ext.dat and b/ctre_sim/CANCoder vers. H - 012 - 0 - ext.dat differ diff --git a/ctre_sim/CANCoder vers. H - 03 - 0 - ext.dat b/ctre_sim/CANCoder vers. H - 03 - 0 - ext.dat index e9780cc..142c29d 100644 Binary files a/ctre_sim/CANCoder vers. H - 03 - 0 - ext.dat and b/ctre_sim/CANCoder vers. H - 03 - 0 - ext.dat differ diff --git a/ctre_sim/CANCoder vers. H - 06 - 0 - ext.dat b/ctre_sim/CANCoder vers. H - 06 - 0 - ext.dat index c4d7863..3db2e15 100644 Binary files a/ctre_sim/CANCoder vers. H - 06 - 0 - ext.dat and b/ctre_sim/CANCoder vers. H - 06 - 0 - ext.dat differ diff --git a/ctre_sim/CANCoder vers. H - 09 - 0 - ext.dat b/ctre_sim/CANCoder vers. H - 09 - 0 - ext.dat index 7500340..f3b2389 100644 Binary files a/ctre_sim/CANCoder vers. H - 09 - 0 - ext.dat and b/ctre_sim/CANCoder vers. H - 09 - 0 - ext.dat differ diff --git a/ctre_sim/Pigeon 2 - 00 - 0 - ext.dat b/ctre_sim/Pigeon 2 - 00 - 0 - ext.dat index 8e078d2..7b090d0 100644 Binary files a/ctre_sim/Pigeon 2 - 00 - 0 - ext.dat and b/ctre_sim/Pigeon 2 - 00 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 01 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 01 - 0 - ext.dat index 47dddf1..292d4d8 100644 Binary files a/ctre_sim/Talon FX vers. C - 01 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 01 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 010 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 010 - 0 - ext.dat index 4aeac8c..84250c9 100644 Binary files a/ctre_sim/Talon FX vers. C - 010 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 010 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 011 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 011 - 0 - ext.dat index 5475aad..a802c39 100644 Binary files a/ctre_sim/Talon FX vers. C - 011 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 011 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 014 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 014 - 0 - ext.dat index 9cbd467..56d38ad 100644 Binary files a/ctre_sim/Talon FX vers. C - 014 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 014 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 015 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 015 - 0 - ext.dat index 7747eed..04fd610 100644 Binary files a/ctre_sim/Talon FX vers. C - 015 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 015 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 016 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 016 - 0 - ext.dat index 77d5687..ac41688 100644 Binary files a/ctre_sim/Talon FX vers. C - 016 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 016 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 02 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 02 - 0 - ext.dat index 9638254..abae529 100644 Binary files a/ctre_sim/Talon FX vers. C - 02 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 02 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 04 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 04 - 0 - ext.dat index 3e1c4b9..ece2580 100644 Binary files a/ctre_sim/Talon FX vers. C - 04 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 04 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 05 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 05 - 0 - ext.dat index e1663b8..a3048d6 100644 Binary files a/ctre_sim/Talon FX vers. C - 05 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 05 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 07 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 07 - 0 - ext.dat index 23772e2..ef2eddf 100644 Binary files a/ctre_sim/Talon FX vers. C - 07 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 07 - 0 - ext.dat differ diff --git a/ctre_sim/Talon FX vers. C - 08 - 0 - ext.dat b/ctre_sim/Talon FX vers. C - 08 - 0 - ext.dat index 4f9652d..1cee9c8 100644 Binary files a/ctre_sim/Talon FX vers. C - 08 - 0 - ext.dat and b/ctre_sim/Talon FX vers. C - 08 - 0 - ext.dat differ diff --git a/logs/sim_2025-02-03_16-51-10.hoot b/logs/sim_2025-02-03_16-51-10.hoot deleted file mode 100644 index 9bbb108..0000000 Binary files a/logs/sim_2025-02-03_16-51-10.hoot and /dev/null differ diff --git a/reefcontrols-site-old/NT4.js b/reefcontrols-site-old/NT4.js new file mode 100644 index 0000000..cd8f09a --- /dev/null +++ b/reefcontrols-site-old/NT4.js @@ -0,0 +1,555 @@ +// Copyright (c) 2025 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +import { serialize, deserialize } from "./msgpack.js"; +const typestrIdxLookup = { + boolean: 0, + double: 1, + int: 2, + float: 3, + string: 4, + json: 4, + raw: 5, + rpc: 5, + msgpack: 5, + protobuf: 5, + "boolean[]": 16, + "double[]": 17, + "int[]": 18, + "float[]": 19, + "string[]": 20, +}; +class NT4_Subscription { + uid = -1; + topics = new Set(); + options = new NT4_SubscriptionOptions(); + toSubscribeObj() { + return { + topics: Array.from(this.topics), + subuid: this.uid, + options: this.options.toObj(), + }; + } + toUnsubscribeObj() { + return { + subuid: this.uid, + }; + } +} +class NT4_SubscriptionOptions { + periodic = 0.1; + all = false; + topicsOnly = false; + prefix = false; + toObj() { + return { + periodic: this.periodic, + all: this.all, + topicsonly: this.topicsOnly, + prefix: this.prefix, + }; + } +} +export class NT4_Topic { + uid = -1; // "id" if server topic, "pubuid" if published + name = ""; + type = ""; + properties = {}; + toPublishObj() { + return { + name: this.name, + type: this.type, + pubuid: this.uid, + properties: this.properties, + }; + } + toUnpublishObj() { + return { + pubuid: this.uid, + }; + } + getTypeIdx() { + if (this.type in typestrIdxLookup) { + return typestrIdxLookup[this.type]; + } else { + return 5; // Default to binary + } + } +} +export class NT4_Client { + appName; + onTopicAnnounce; + onTopicUnannounce; + onNewTopicData; + onConnect; + onDisconnect; + serverBaseAddr; + ws = null; + serverAddr = ""; + serverConnectionActive = false; + serverConnectionRequested = false; + serverTimeOffset_us = null; + networkLatency_us = 0; + rxLengthCounter = 0; + subscriptions = new Map(); + publishedTopics = new Map(); + serverTopics = new Map(); + /** + * Creates a new NT4 client without connecting. + * @param serverAddr Network address of NT4 server + * @param appName Identifier for this client (does not need to be unique). + * @param onTopicAnnounce Gets called when server announces enough topics to form a new signal + * @param onTopicUnannounce Gets called when server unannounces any part of a signal + * @param onNewTopicData Gets called when any new data is available + * @param onConnect Gets called once client completes initial handshake with server + * @param onDisconnect Gets called once client detects server has disconnected + */ + constructor( + serverAddr, + appName, + onTopicAnnounce, + onTopicUnannounce, + onNewTopicData, + onConnect, + onDisconnect + ) { + this.serverBaseAddr = serverAddr; + this.appName = appName; + this.onTopicAnnounce = onTopicAnnounce; + this.onTopicUnannounce = onTopicUnannounce; + this.onNewTopicData = onNewTopicData; + this.onConnect = onConnect; + this.onDisconnect = onDisconnect; + setInterval(() => { + // Update timestamp + this.ws_sendTimestamp(); + // Log bitrate + let bitrateKbPerSec = ((this.rxLengthCounter / 1000) * 8) / 5; + this.rxLengthCounter = 0; + console.log( + "[NT4] Bitrate: " + Math.round(bitrateKbPerSec).toString() + " kb/s" + ); + }, 5000); + } + ////////////////////////////////////////////////////////////// + // PUBLIC API + /** Starts the connection. The client will reconnect automatically when disconnected. */ + connect() { + if (!this.serverConnectionRequested) { + this.serverConnectionRequested = true; + this.ws_connect(); + } + } + /** Terminates the connection. */ + disconnect() { + if (this.serverConnectionRequested) { + this.serverConnectionRequested = false; + if (this.serverConnectionActive && this.ws) { + this.ws.close(); + } + } + } + /** + * Add a new subscription, reading value updates + * @param topicPatterns A list of topics or prefixes to include in the subscription. + * @param prefixMode If true, use patterns as prefixes. If false, only subscribe to topics that are an exact match. + * @param sendAll If true, send all values. If false, only send the most recent value. + * @param periodic How frequently to send updates (applies regardless of "sendAll" option) + * @returns A subscription ID that can be used to unsubscribe. + */ + subscribe(topicPatterns, prefixMode, sendAll = false, periodic = 0.1) { + let newSub = new NT4_Subscription(); + newSub.uid = this.getNewUID(); + newSub.topics = new Set(topicPatterns); + newSub.options.prefix = prefixMode; + newSub.options.all = sendAll; + newSub.options.periodic = periodic; + this.subscriptions.set(newSub.uid, newSub); + if (this.serverConnectionActive) { + this.ws_subscribe(newSub); + } + return newSub.uid; + } + /** + * Add a new subscription, reading only topic announcements (not values). + * @param topicPatterns A list of topics or prefixes to include in the subscription. + * @param prefixMode If true, use patterns as prefixes. If false, only subscribe to topics that are an exact match. + * @returns A subscription ID that can be used to unsubscribe. + */ + subscribeTopicsOnly(topicPatterns, prefixMode) { + let newSub = new NT4_Subscription(); + newSub.uid = this.getNewUID(); + newSub.topics = new Set(topicPatterns); + newSub.options.prefix = prefixMode; + newSub.options.topicsOnly = true; + this.subscriptions.set(newSub.uid, newSub); + if (this.serverConnectionActive) { + this.ws_subscribe(newSub); + } + return newSub.uid; + } + /** Given an existing subscription, unsubscribe from it. */ + unsubscribe(subscriptionId) { + let subscription = this.subscriptions.get(subscriptionId); + if (!subscription) { + throw 'Unknown subscription ID "' + subscriptionId + '"'; + } + this.subscriptions.delete(subscriptionId); + if (this.serverConnectionActive) { + this.ws_unsubscribe(subscription); + } + } + /** Unsubscribe from all current subscriptions. */ + clearAllSubscriptions() { + for (const subscriptionId of this.subscriptions.keys()) { + this.unsubscribe(subscriptionId); + } + } + /** + * Set the properties of a particular topic. + * @param topic The topic to update + * @param properties The set of new properties + */ + setProperties(topic, properties) { + // Update local topics + let updateTopic = (toUpdate) => { + for (const key of Object.keys(properties)) { + let value = properties[key]; + if (value === null) { + delete toUpdate.properties[key]; + } else { + toUpdate.properties[key] = value; + } + } + }; + let publishedTopic = this.publishedTopics.get(topic); + if (publishedTopic) updateTopic(publishedTopic); + let serverTopic = this.serverTopics.get(topic); + if (serverTopic) updateTopic(serverTopic); + // Send new properties to server + if (this.serverConnectionActive) { + this.ws_setproperties(topic, properties); + } + } + /** Set whether a topic is persistent. + * + * If true, the last set value will be periodically saved to + * persistent storage on the server and be restored during server + * startup. Topics with this property set to true will not be + * deleted by the server when the last publisher stops publishing. + */ + setPersistent(topic, isPersistent) { + this.setProperties(topic, { persistent: isPersistent }); + } + /** Set whether a topic is retained. + * + * Topics with this property set to true will not be deleted by + * the server when the last publisher stops publishing. + */ + setRetained(topic, isRetained) { + this.setProperties(topic, { retained: isRetained }); + } + /** Publish a topic from this client with the provided name and type. Can be a new or existing. */ + publishTopic(topic, type) { + if (this.publishedTopics.has(topic)) { + return; + } + let newTopic = new NT4_Topic(); + newTopic.name = topic; + newTopic.uid = this.getNewUID(); + newTopic.type = type; + this.publishedTopics.set(topic, newTopic); + if (this.serverConnectionActive) { + this.ws_publish(newTopic); + } + return; + } + /** Unpublish a previously-published topic from this client. */ + unpublishTopic(topic) { + let topicObj = this.publishedTopics.get(topic); + if (!topicObj) { + throw 'Topic "' + topic + '" not found'; + } + this.publishedTopics.delete(topic); + if (this.serverConnectionActive) { + this.ws_unpublish(topicObj); + } + } + /** Send some new value to the server. The timestamp is whatever the current time is. */ + addSample(topic, value) { + let timestamp = this.getServerTime_us(); + if (timestamp === null) timestamp = 0; + this.addTimestampedSample(topic, timestamp, value); + } + /** Send some new timestamped value to the server. */ + addTimestampedSample(topic, timestamp, value) { + let topicObj = this.publishedTopics.get(topic); + if (!topicObj) { + throw 'Topic "' + topic + '" not found'; + } + let txData = serialize([ + topicObj.uid, + timestamp, + topicObj.getTypeIdx(), + value, + ]); + this.ws_sendBinary(txData); + } + ////////////////////////////////////////////////////////////// + // Server/Client Time Sync Handling + /** Returns the current client time in microseconds. */ + getClientTime_us() { + return new Date().getTime() * 1000; + } + /** Returns the current server time in microseconds (or null if unknown). */ + getServerTime_us(clientTime) { + if (this.serverTimeOffset_us === null) { + return null; + } else { + return ( + (clientTime === undefined ? this.getClientTime_us() : clientTime) + + this.serverTimeOffset_us + ); + } + } + /** Returns the current network latency in microseconds */ + getNetworkLatency_us() { + return this.networkLatency_us; + } + ws_sendTimestamp() { + let timeToSend = this.getClientTime_us(); + let txData = serialize([-1, 0, typestrIdxLookup["int"], timeToSend]); + this.ws_sendBinary(txData); + } + ws_handleReceiveTimestamp(serverTimestamp, clientTimestamp) { + let rxTime = this.getClientTime_us(); + // Recalculate server/client offset based on round trip time + let rtt = rxTime - clientTimestamp; + this.networkLatency_us = rtt / 2.0; + let serverTimeAtRx = serverTimestamp + this.networkLatency_us; + this.serverTimeOffset_us = serverTimeAtRx - rxTime; + console.log( + "[NT4] New server time: " + + (this.getServerTime_us() / 1000000.0).toString() + + "s with " + + (this.networkLatency_us / 1000.0).toString() + + "ms latency" + ); + } + ////////////////////////////////////////////////////////////// + // Websocket Message Send Handlers + ws_subscribe(subscription) { + this.ws_sendJSON("subscribe", subscription.toSubscribeObj()); + } + ws_unsubscribe(subscription) { + this.ws_sendJSON("unsubscribe", subscription.toUnsubscribeObj()); + } + ws_publish(topic) { + this.ws_sendJSON("publish", topic.toPublishObj()); + } + ws_unpublish(topic) { + this.ws_sendJSON("unpublish", topic.toUnpublishObj()); + } + ws_setproperties(topic, newProperties) { + this.ws_sendJSON("setproperties", { + name: topic, + update: newProperties, + }); + } + ws_sendJSON(method, params) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send( + JSON.stringify([ + { + method: method, + params: params, + }, + ]) + ); + } + } + ws_sendBinary(data) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(data); + } + } + ////////////////////////////////////////////////////////////// + // Websocket connection Maintenance + ws_onOpen() { + // Set the flag allowing general server communication + this.serverConnectionActive = true; + console.log('[NT4] Connected with identity "' + this.appName + '"'); + // Sync timestamps + this.ws_sendTimestamp(); + // Publish any existing topics + for (const topic of this.publishedTopics.values()) { + this.ws_publish(topic); + } + // Subscribe to existing subscriptions + for (const subscription of this.subscriptions.values()) { + this.ws_subscribe(subscription); + } + // User connection-opened hook + this.onConnect(); + } + ws_onClose(event) { + // Clear flags to stop server communication + this.ws = null; + this.serverConnectionActive = false; + // User connection-closed hook + this.onDisconnect(); + // Clear out any local cache of server state + this.serverTopics.clear(); + if (event.reason !== "") { + console.log("[NT4] Socket is closed: ", event.reason); + } + if (this.serverConnectionRequested) { + setTimeout(() => this.ws_connect(), 500); + } + } + ws_onError() { + if (this.ws) this.ws.close(); + } + ws_onMessage(event) { + if (typeof event.data === "string") { + // JSON array + this.rxLengthCounter += event.data.length; + let msgData = JSON.parse(event.data); + if (!Array.isArray(msgData)) { + console.warn( + "[NT4] Ignoring text message, JSON parsing did not produce an array at the top level." + ); + return; + } + msgData.forEach((msg) => { + // Validate proper format of message + if (typeof msg !== "object") { + console.warn( + "[NT4] Ignoring text message, JSON parsing did not produce an object." + ); + return; + } + if (!("method" in msg) || !("params" in msg)) { + console.warn( + "[NT4] Ignoring text message, JSON parsing did not find all required fields." + ); + return; + } + let method = msg["method"]; + let params = msg["params"]; + if (typeof method !== "string") { + console.warn( + '[NT4] Ignoring text message, JSON parsing found "method", but it wasn\'t a string.' + ); + return; + } + if (typeof params !== "object") { + console.warn( + '[NT4] Ignoring text message, JSON parsing found "params", but it wasn\'t an object.' + ); + return; + } + // Message validates reasonably, switch based on supported methods + if (method === "announce") { + let newTopic = new NT4_Topic(); + newTopic.uid = params.id; + newTopic.name = params.name; + newTopic.type = params.type; + newTopic.properties = params.properties; + this.serverTopics.set(newTopic.name, newTopic); + this.onTopicAnnounce(newTopic); + } else if (method === "unannounce") { + let removedTopic = this.serverTopics.get(params.name); + if (!removedTopic) { + console.warn( + "[NT4] Ignoring unannounce, topic was not previously announced." + ); + return; + } + this.serverTopics.delete(removedTopic.name); + this.onTopicUnannounce(removedTopic); + } else if (method === "properties") { + let topic = this.serverTopics.get(params.name); + if (!topic) { + console.warn( + "[NT4] Ignoring set properties, topic was not previously announced." + ); + return; + } + for (const key of Object.keys(params.update)) { + let value = params.update[key]; + if (value === null) { + delete topic.properties[key]; + } else { + topic.properties[key] = value; + } + } + } else { + console.warn( + "[NT4] Ignoring text message - unknown method " + method + ); + return; + } + }); + } else { + // MSGPack + this.rxLengthCounter += event.data.byteLength; + deserialize(event.data, { multiple: true }).forEach((unpackedData) => { + let topicID = unpackedData[0]; + let timestamp_us = unpackedData[1]; + let typeIdx = unpackedData[2]; + let value = unpackedData[3]; + if (topicID >= 0) { + let topic = null; + for (let serverTopic of this.serverTopics.values()) { + if (serverTopic.uid === topicID) { + topic = serverTopic; + break; + } + } + if (!topic) { + console.warn( + "[NT4] Ignoring binary data - unknown topic ID " + + topicID.toString() + ); + return; + } + this.onNewTopicData(topic, timestamp_us, value); + } else if (topicID === -1) { + this.ws_handleReceiveTimestamp(timestamp_us, value); + } else { + console.warn( + "[NT4] Ignoring binary data - invalid topic ID " + + topicID.toString() + ); + } + }); + } + } + ws_connect() { + let port = 5810; + let prefix = "ws://"; + this.serverAddr = + prefix + + this.serverBaseAddr + + ":" + + port.toString() + + "/nt/" + + this.appName; + this.ws = new WebSocket(this.serverAddr, "networktables.first.wpi.edu"); + this.ws.binaryType = "arraybuffer"; + this.ws.addEventListener("open", () => this.ws_onOpen()); + this.ws.addEventListener("message", (event) => this.ws_onMessage(event)); + this.ws.addEventListener("close", (event) => this.ws_onClose(event)); + this.ws.addEventListener("error", () => this.ws_onError()); + } + ////////////////////////////////////////////////////////////// + // General utilities + getNewUID() { + return Math.floor(Math.random() * 99999999); + } +} diff --git a/reefcontrols-site-old/index.css b/reefcontrols-site-old/index.css new file mode 100644 index 0000000..cfcc378 --- /dev/null +++ b/reefcontrols-site-old/index.css @@ -0,0 +1,315 @@ +/* + Copyright (c) 2025 FRC 6328 + http://github.com/Mechanical-Advantage + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file at + the root directory of this project. +*/ + +body { + overscroll-behavior: none; + overflow: hidden; + font-family: Helvetica, sans-serif; + user-select: none; + -webkit-user-select: none; + cursor: default; + color: #000; + + /* Show disconnected state by default */ + background-color: red; +} + +div { + position: absolute; +} + +div.overview-section { + top: 0%; + left: 0%; + height: 100%; + width: 15%; +} + +div.overview-section div.counter-container { + left: 0%; + width: 100%; + height: 25%; +} + +div.overview-section div.counter-container:nth-child(1) { + top: 75%; +} + +div.overview-section div.counter-container:nth-child(2) { + top: 50%; +} + +div.overview-section div.counter-container:nth-child(3) { + top: 25%; +} + +div.overview-section div.counter-container:nth-child(4) { + top: 0%; +} + +div.overview-section div.counter-container div.counter-area { + top: 10%; + height: 80%; + left: 10%; + width: 80%; + border-radius: 5vh; + border: 1px solid #333; +} + +div.overview-section div.counter-container div.counter-area.active { + background-color: #ffffff; +} + +div.overview-section div.counter-container div.counter-area div { + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 10vh; +} + +div.reef-section { + top: 0%; + left: 15%; + height: 80%; + width: 85%; +} + +div.reef-section canvas { + position: absolute; + left: 0%; + top: 0%; + width: 100%; + height: 100%; + z-index: 0; +} + +div.reef-section div.branch, +div.reef-section div.algae { + border-radius: 100%; + transform: translate(-50%, -50%); + border: 1px solid #333; + z-index: 1; +} + +div.reef-section div.branch { + height: 12vh; + width: 12vh; +} + +div.reef-section div.algae { + height: 14vh; + width: 14vh; +} + +div.reef-section div.branch { + background-color: #f4c9ff; +} + +div.reef-section div.branch.active { + background-color: #cc00ff; +} + +div.reef-section div.branch img { + display: none; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 10vh; +} + +div.reef-section div.branch.active img { + display: initial; +} + +div.reef-section div.algae { + background-color: #c6efff; +} + +div.reef-section div.algae.active { + background-color: #00b7ff; +} + +div.reef-section div.algae img { + display: none; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 10vh; +} + +div.reef-section div.algae.active img { + display: initial; +} + +div.reef-section div.branch:nth-child(1) { + left: calc(50% + 10%); + top: calc(50% + 40%); +} + +div.reef-section div.branch:nth-child(2) { + left: calc(50% - 10%); + top: calc(50% + 40%); +} + +div.reef-section div.branch:nth-child(3) { + left: calc(50% - 30%); + top: calc(50% + 30%); +} + +div.reef-section div.branch:nth-child(4) { + left: calc(50% - 40%); + top: calc(50% + 12%); +} + +div.reef-section div.branch:nth-child(5) { + left: calc(50% - 40%); + top: calc(50% - 12%); +} + +div.reef-section div.branch:nth-child(6) { + left: calc(50% - 30%); + top: calc(50% - 30%); +} + +div.reef-section div.branch:nth-child(7) { + left: calc(50% - 10%); + top: calc(50% - 40%); +} + +div.reef-section div.branch:nth-child(8) { + left: calc(50% + 10%); + top: calc(50% - 40%); +} + +div.reef-section div.branch:nth-child(9) { + left: calc(50% + 30%); + top: calc(50% - 30%); +} + +div.reef-section div.branch:nth-child(10) { + left: calc(50% + 40%); + top: calc(50% - 12%); +} + +div.reef-section div.branch:nth-child(11) { + left: calc(50% + 40%); + top: calc(50% + 12%); +} + +div.reef-section div.branch:nth-child(12) { + left: calc(50% + 30%); + top: calc(50% + 30%); +} + +div.reef-section div.algae:nth-child(13) { + left: calc(50% + 0%); + top: calc(50% + 20%); +} + +div.reef-section div.algae:nth-child(14) { + left: calc(50% - 18%); + top: calc(50% + 10%); +} + +div.reef-section div.algae:nth-child(15) { + left: calc(50% - 18%); + top: calc(50% - 10%); +} + +div.reef-section div.algae:nth-child(16) { + left: calc(50% + 0%); + top: calc(50% - 20%); +} + +div.reef-section div.algae:nth-child(17) { + left: calc(50% + 18%); + top: calc(50% - 10%); +} + +div.reef-section div.algae:nth-child(18) { + left: calc(50% + 18%); + top: calc(50% + 10%); +} + +div.reef-section div.flag { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 12vh; + z-index: 1; +} + +div.lower-section { + top: 80%; + left: 15%; + height: 20%; + width: 85%; +} + +div.lower-section div.subtract, +div.lower-section div.add, +div.lower-section div.coop { + top: 10%; + height: 80%; + width: 25%; + border-radius: 5vh; + border: 1px solid #333; +} + +div.lower-section div.subtract { + left: 7%; +} + +div.lower-section div.add { + left: 34%; +} + +div.lower-section div.coop { + left: 68%; +} + +div.lower-section div.coop.active { + background-color: #00b7ff; +} + +div.lower-section div.subtract div, +div.lower-section div.add div { + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 10vh; + filter: brightness(0); +} + +div.lower-section div.coop div { + display: none; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 12vh; +} + +div.lower-section div.coop.active div { + display: initial; +} + +body.elims div.lower-section div.subtract { + left: 24%; +} + +body.elims div.lower-section div.add { + left: 51%; +} + +body.elims div.lower-section div.coop { + display: none; +} \ No newline at end of file diff --git a/reefcontrols-site-old/index.html b/reefcontrols-site-old/index.html new file mode 100644 index 0000000..95008a0 --- /dev/null +++ b/reefcontrols-site-old/index.html @@ -0,0 +1,67 @@ + + + + + + + + + + Reef Controls — FRC 9470 + + +
+
+
0
+
+
+
0
+
+
+
0
+
+
+
0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
🤝
+
+ + \ No newline at end of file diff --git a/reefcontrols-site-old/index.js b/reefcontrols-site-old/index.js new file mode 100644 index 0000000..4be0e63 --- /dev/null +++ b/reefcontrols-site-old/index.js @@ -0,0 +1,284 @@ +// Copyright (c) 2025 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file at +// the root directory of this project. + +import { NT4_Client } from "./NT4.js"; + +// ***** NETWORKTABLES ***** + +// const toRobotPrefix = "/ReefControls/ToRobot/"; +// const toDashboardPrefix = "/ReefControls/ToDashboard/"; +// const selectedLevelTopicName = "SelectedLevel"; +// const l1TopicName = "Level1"; +// const l2TopicName = "Level2"; +// const l3TopicName = "Level3"; +// const l4TopicName = "Level4"; +// const algaeTopicName = "Algae"; +// const coopTopicName = "Coop"; +// const isElimsTopicName = "IsElims"; + +const ntClient = new NT4_Client( + window.location.hostname, + "Gamma2025", + () => { + // Topic announce + }, + () => { + // Topic unannounce + }, + (topic, _, value) => { + // New data + }, + () => { + // Connected + document.body.style.backgroundColor = "white"; + }, + () => { + // Disconnected + document.body.style.backgroundColor = "red"; + } +); + +// Start NT connection +window.addEventListener("load", () => { + ntClient.publishTopic("branch", "int"); + ntClient.publishTopic("level", "int"); + ntClient.connect(); +}); + +// ***** STATE CACHE ***** + +let selectedLevel = 0; // 0 = L2, 1 = L3, 2 = L4 +let l1State = 0; // Count +let l2State = 0; // Bitfield +let l3State = 0; // Bitfield +let l4State = 0; // Bitfield +let algaeState = 0; // Bitfield +let coopState = false; // Boolean +let isElims = false; // Boolean + +/** Update the full UI based on the state cache. */ +function updateUI() { + // Update counter highlight + Array.from(document.getElementsByClassName("counter-area")).forEach( + (element, index) => { + if (index > 0 && selectedLevel === index - 1) { + element.classList.add("active"); + } else { + element.classList.remove("active"); + } + } + ); + + // Update background color + switch (selectedLevel) { + case 0: + document.body.style.backgroundColor = "#ccffff"; + break; + case 1: + document.body.style.backgroundColor = "#b5e5ff"; + break; + case 2: + document.body.style.backgroundColor = "#dfbbfc"; + break; + } + + // Update level counts + let rpLevelCount = 0; + Array.from(document.getElementsByClassName("counter")).forEach( + (element, index) => { + if (index === 0) { + element.innerText = l1State; + if (l1State >= 5) rpLevelCount++; + } else { + let count = 0; + let levelState = [l2State, l3State, l4State][index - 1]; + for (let i = 0; i < 12; i++) { + if (((1 << i) & levelState) > 0) { + count++; + } + } + element.innerText = count === 12 ? "\u2705" : count; + if (count >= 5) rpLevelCount++; + } + } + ); + + // Update coral buttons + Array.from(document.getElementsByClassName("branch")).forEach( + (element, index) => { + let levelState = [l2State, l3State, l4State][selectedLevel]; + if (((1 << index) & levelState) > 0) { + element.classList.add("active"); + } else { + element.classList.remove("active"); + } + } + ); + + // Update algae buttons + Array.from(document.getElementsByClassName("algae")).forEach( + (element, index) => { + if (((1 << index) & algaeState) > 0) { + element.classList.add("active"); + } else { + element.classList.remove("active"); + } + } + ); + + // Update coop button + let coopDiv = document.getElementsByClassName("coop")[0]; + if (coopState) { + coopDiv.classList.add("active"); + } else { + coopDiv.classList.remove("active"); + } + + // Update RP flag + document.getElementsByClassName("flag")[0].hidden = + isElims || rpLevelCount < (coopState ? 3 : 4); + + // Update elims state + if (isElims) { + document.body.classList.add("elims"); + } else { + document.body.classList.remove("elims"); + } +} + +// ***** BUTTON BINDINGS ***** + +let isTouch = false; + +function bind(element, callback) { + let activate = (touchEvent) => { + if (touchEvent) { + isTouch = true; + } + if (isTouch == touchEvent) { + callback(); + } + }; + + element.addEventListener("touchstart", () => activate(true)); + element.addEventListener("mousedown", () => activate(false)); + element.addEventListener("contextmenu", (event) => { + event.preventDefault(); + activate(false); + }); +} + +let lastMouseEvent = 0; +window.addEventListener("mousemove", () => { + let now = new Date().getTime(); + if (now - lastMouseEvent < 50) { + isTouch = false; + } + lastMouseEvent = now; +}); + +window.addEventListener("load", () => { + // Buttons to change selected level + Array.from(document.getElementsByClassName("counter-area")).forEach( + (element, index) => { + if (index > 0) { + bind(element, () => { + selectedLevel = index; + }); + } + } + ); + + // Coral toggle buttons + Array.from(document.getElementsByClassName("branch")).forEach( + (element, index) => { + bind(element, () => { + ntClient.addSample( + "branch", + index + ); + ntClient.addSample( + "level", + selectedLevel + ); + }); + } + ); + + // // Algae toggle buttons + // Array.from(document.getElementsByClassName("algae")).forEach( + // (element, index) => { + // bind(element, () => { + // ntClient.addSample( + // toRobotPrefix + algaeTopicName, + // algaeState ^ (1 << index) + // ); + // }); + // } + // ); + + // // L1 count controls + // bind(document.getElementsByClassName("subtract")[0], () => { + // if (l1State > 0) { + // ntClient.addSample(toRobotPrefix + l1TopicName, l1State - 1); + // } + // }); + // bind(document.getElementsByClassName("add")[0], () => { + // ntClient.addSample(toRobotPrefix + l1TopicName, l1State + 1); + // }); + + // // Coop button + // bind(document.getElementsByClassName("coop")[0], () => { + // ntClient.addSample(toRobotPrefix + coopTopicName, !coopState); + // }); +}); + +// ***** REEF CANVAS ***** + +window.addEventListener("load", () => { + const canvas = document.getElementsByTagName("canvas")[0]; + const context = canvas.getContext("2d"); + + let render = () => { + const devicePixelRatio = window.devicePixelRatio; + const width = canvas.clientWidth; + const height = canvas.clientHeight; + canvas.width = width * devicePixelRatio; + canvas.height = height * devicePixelRatio; + context.scale(devicePixelRatio, devicePixelRatio); + context.clearRect(0, 0, width, height); + + const corners = [ + [width * 0.74, height * 0.9], + [width * 0.26, height * 0.9], + [width * 0.03, height * 0.5], + [width * 0.26, height * 0.1], + [width * 0.74, height * 0.1], + [width * 0.97, height * 0.5], + ]; + + context.beginPath(); + corners.forEach((corner) => { + context.moveTo(width * 0.5, height * 0.5); + context.lineTo(...corner); + }); + corners.forEach((corner, index) => { + if (index == 0) { + context.moveTo(...corner); + } else { + context.lineTo(...corner); + } + }); + context.closePath(); + + context.strokeStyle = "black"; + context.stroke(); + }; + + render(); + window.addEventListener("resize", render); +}); diff --git a/reefcontrols-site-old/msgpack.js b/reefcontrols-site-old/msgpack.js new file mode 100644 index 0000000..ecf20ee --- /dev/null +++ b/reefcontrols-site-old/msgpack.js @@ -0,0 +1,518 @@ +// Serializes a value to a MessagePack byte array. +// +// data: The value to serialize. This can be a scalar, array or object. +// options: An object that defined additional options. +// - multiple: Indicates whether multiple values in data are concatenated to multiple MessagePack arrays. +// - invalidTypeReplacement: The value that is used to replace values of unsupported types, or a function that returns such a value, given the original value as parameter. +export function serialize(data, options) { + if (options && options.multiple && !Array.isArray(data)) { + throw new Error("Invalid argument type: Expected an Array to serialize multiple values."); + } + const pow32 = 0x100000000; // 2^32 + let floatBuffer, floatView; + let array = new Uint8Array(128); + let length = 0; + + let th = ""; + if (options && options.typeHint) { + th = options.typeHint; + } + + if (options && options.multiple) { + for (let i = 0; i < data.length; i++) { + append(data[i], false, th); + } + } else { + append(data, false, th); + } + return array.subarray(0, length); + + function append(data, isReplacement, th) { + switch (typeof data) { + case "undefined": + appendNull(data); + break; + case "boolean": + appendBoolean(data); + break; + case "number": + appendNumber(data, th); + break; + case "string": + appendString(data); + break; + case "object": + if (data === null) appendNull(data); + else if (data instanceof Date) appendDate(data); + else if (Array.isArray(data)) appendArray(data); + else if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) appendBinArray(data); + else if ( + data instanceof Int8Array || + data instanceof Int16Array || + data instanceof Uint16Array || + data instanceof Int32Array || + data instanceof Uint32Array || + data instanceof Float32Array || + data instanceof Float64Array + ) + appendArray(data); + else appendObject(data); + break; + default: + if (!isReplacement && options && options.invalidTypeReplacement) { + if (typeof options.invalidTypeReplacement === "function") + append(options.invalidTypeReplacement(data), true, th); + else append(options.invalidTypeReplacement, true, th); + } else { + throw new Error("Invalid argument type: The type '" + typeof data + "' cannot be serialized."); + } + } + } + + function appendNull(data) { + appendByte(0xc0); + } + + function appendBoolean(data) { + appendByte(data ? 0xc3 : 0xc2); + } + + function appendNumber(data, th) { + const isInteger = + th === "int" || (isFinite(data) && Math.floor(data) === data && th !== "double" && th !== "float"); + if (isInteger) { + // Integer + if (data >= 0 && data <= 0x7f) { + appendByte(data); + } else if (data < 0 && data >= -0x20) { + appendByte(data); + } else if (data > 0 && data <= 0xff) { + // uint8 + appendBytes([0xcc, data]); + } else if (data >= -0x80 && data <= 0x7f) { + // int8 + appendBytes([0xd0, data]); + } else if (data > 0 && data <= 0xffff) { + // uint16 + appendBytes([0xcd, data >>> 8, data]); + } else if (data >= -0x8000 && data <= 0x7fff) { + // int16 + appendBytes([0xd1, data >>> 8, data]); + } else if (data > 0 && data <= 0xffffffff) { + // uint32 + appendBytes([0xce, data >>> 24, data >>> 16, data >>> 8, data]); + } else if (data >= -0x80000000 && data <= 0x7fffffff) { + // int32 + appendBytes([0xd2, data >>> 24, data >>> 16, data >>> 8, data]); + } else if (data > 0 && data <= 0xffffffffffffffff) { + // uint64 + // Split 64-bit number into two 32-bit numbers because JavaScript only regards + // 32 bits for bitwise operations. + let hi = data / pow32; + let lo = data % pow32; + appendBytes([0xd3, hi >>> 24, hi >>> 16, hi >>> 8, hi, lo >>> 24, lo >>> 16, lo >>> 8, lo]); + } else if (data >= -0x8000000000000000 && data <= 0x7fffffffffffffff) { + // int64 + appendByte(0xd3); + appendInt64(data); + } else if (data < 0) { + // below int64 + appendBytes([0xd3, 0x80, 0, 0, 0, 0, 0, 0, 0]); + } else { + // above uint64 + appendBytes([0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + } + } else { + // Float + if (!floatView) { + floatBuffer = new ArrayBuffer(8); + floatView = new DataView(floatBuffer); + } + floatView.setFloat64(0, data); + appendByte(0xcb); + appendBytes(new Uint8Array(floatBuffer)); + } + } + + function appendString(data) { + let bytes = encodeUtf8(data); + let length = bytes.length; + + if (length <= 0x1f) appendByte(0xa0 + length); + else if (length <= 0xff) appendBytes([0xd9, length]); + else if (length <= 0xffff) appendBytes([0xda, length >>> 8, length]); + else appendBytes([0xdb, length >>> 24, length >>> 16, length >>> 8, length]); + + appendBytes(bytes); + } + + function appendArray(data) { + let length = data.length; + + if (length <= 0xf) appendByte(0x90 + length); + else if (length <= 0xffff) appendBytes([0xdc, length >>> 8, length]); + else appendBytes([0xdd, length >>> 24, length >>> 16, length >>> 8, length]); + + for (let index = 0; index < length; index++) { + append(data[index]); + } + } + + function appendBinArray(data) { + let length = data.length; + + if (length <= 0xf) appendBytes([0xc4, length]); + else if (length <= 0xffff) appendBytes([0xc5, length >>> 8, length]); + else appendBytes([0xc6, length >>> 24, length >>> 16, length >>> 8, length]); + + appendBytes(data); + } + + function appendObject(data) { + let length = 0; + for (let key in data) { + if (data[key] !== undefined) { + length++; + } + } + + if (length <= 0xf) appendByte(0x80 + length); + else if (length <= 0xffff) appendBytes([0xde, length >>> 8, length]); + else appendBytes([0xdf, length >>> 24, length >>> 16, length >>> 8, length]); + + for (let key in data) { + let value = data[key]; + if (value !== undefined) { + append(key); + append(value); + } + } + } + + function appendDate(data) { + let sec = data.getTime() / 1000; + if (data.getMilliseconds() === 0 && sec >= 0 && sec < 0x100000000) { + // 32 bit seconds + appendBytes([0xd6, 0xff, sec >>> 24, sec >>> 16, sec >>> 8, sec]); + } else if (sec >= 0 && sec < 0x400000000) { + // 30 bit nanoseconds, 34 bit seconds + let ns = data.getMilliseconds() * 1000000; + appendBytes([ + 0xd7, + 0xff, + ns >>> 22, + ns >>> 14, + ns >>> 6, + ((ns << 2) >>> 0) | (sec / pow32), + sec >>> 24, + sec >>> 16, + sec >>> 8, + sec + ]); + } else { + // 32 bit nanoseconds, 64 bit seconds, negative values allowed + let ns = data.getMilliseconds() * 1000000; + appendBytes([0xc7, 12, 0xff, ns >>> 24, ns >>> 16, ns >>> 8, ns]); + appendInt64(sec); + } + } + + function appendByte(byte) { + if (array.length < length + 1) { + let newLength = array.length * 2; + while (newLength < length + 1) newLength *= 2; + let newArray = new Uint8Array(newLength); + newArray.set(array); + array = newArray; + } + array[length] = byte; + length++; + } + + function appendBytes(bytes) { + if (array.length < length + bytes.length) { + let newLength = array.length * 2; + while (newLength < length + bytes.length) newLength *= 2; + let newArray = new Uint8Array(newLength); + newArray.set(array); + array = newArray; + } + array.set(bytes, length); + length += bytes.length; + } + + function appendInt64(value) { + // Split 64-bit number into two 32-bit numbers because JavaScript only regards 32 bits for + // bitwise operations. + let hi, lo; + if (value >= 0) { + // Same as uint64 + hi = value / pow32; + lo = value % pow32; + } else { + // Split absolute value to high and low, then NOT and ADD(1) to restore negativity + value++; + hi = Math.abs(value) / pow32; + lo = Math.abs(value) % pow32; + hi = ~hi; + lo = ~lo; + } + appendBytes([hi >>> 24, hi >>> 16, hi >>> 8, hi, lo >>> 24, lo >>> 16, lo >>> 8, lo]); + } +} + +// Deserializes a MessagePack byte array to a value. +// +// array: The MessagePack byte array to deserialize. This must be an Array or Uint8Array containing bytes, not a string. +// options: An object that defined additional options. +// - multiple: Indicates whether multiple concatenated MessagePack arrays are returned as an array. +export function deserialize(array, options) { + const pow32 = 0x100000000; // 2^32 + let pos = 0; + if (array instanceof ArrayBuffer) { + array = new Uint8Array(array); + } + if (typeof array !== "object" || typeof array.length === "undefined") { + throw new Error("Invalid argument type: Expected a byte array (Array or Uint8Array) to deserialize."); + } + if (!array.length) { + throw new Error("Invalid argument: The byte array to deserialize is empty."); + } + if (!(array instanceof Uint8Array)) { + array = new Uint8Array(array); + } + let data; + if (options && options.multiple) { + // Read as many messages as are available + data = []; + while (pos < array.length) { + data.push(read()); + } + } else { + // Read only one message and ignore additional data + data = read(); + } + return data; + + function read() { + const byte = array[pos++]; + if (byte >= 0x00 && byte <= 0x7f) return byte; // positive fixint + if (byte >= 0x80 && byte <= 0x8f) return readMap(byte - 0x80); // fixmap + if (byte >= 0x90 && byte <= 0x9f) return readArray(byte - 0x90); // fixarray + if (byte >= 0xa0 && byte <= 0xbf) return readStr(byte - 0xa0); // fixstr + if (byte === 0xc0) return null; // nil + if (byte === 0xc1) throw new Error("Invalid byte code 0xc1 found."); // never used + if (byte === 0xc2) return false; // false + if (byte === 0xc3) return true; // true + if (byte === 0xc4) return readBin(-1, 1); // bin 8 + if (byte === 0xc5) return readBin(-1, 2); // bin 16 + if (byte === 0xc6) return readBin(-1, 4); // bin 32 + if (byte === 0xc7) return readExt(-1, 1); // ext 8 + if (byte === 0xc8) return readExt(-1, 2); // ext 16 + if (byte === 0xc9) return readExt(-1, 4); // ext 32 + if (byte === 0xca) return readFloat(4); // float 32 + if (byte === 0xcb) return readFloat(8); // float 64 + if (byte === 0xcc) return readUInt(1); // uint 8 + if (byte === 0xcd) return readUInt(2); // uint 16 + if (byte === 0xce) return readUInt(4); // uint 32 + if (byte === 0xcf) return readUInt(8); // uint 64 + if (byte === 0xd0) return readInt(1); // int 8 + if (byte === 0xd1) return readInt(2); // int 16 + if (byte === 0xd2) return readInt(4); // int 32 + if (byte === 0xd3) return readInt(8); // int 64 + if (byte === 0xd4) return readExt(1); // fixext 1 + if (byte === 0xd5) return readExt(2); // fixext 2 + if (byte === 0xd6) return readExt(4); // fixext 4 + if (byte === 0xd7) return readExt(8); // fixext 8 + if (byte === 0xd8) return readExt(16); // fixext 16 + if (byte === 0xd9) return readStr(-1, 1); // str 8 + if (byte === 0xda) return readStr(-1, 2); // str 16 + if (byte === 0xdb) return readStr(-1, 4); // str 32 + if (byte === 0xdc) return readArray(-1, 2); // array 16 + if (byte === 0xdd) return readArray(-1, 4); // array 32 + if (byte === 0xde) return readMap(-1, 2); // map 16 + if (byte === 0xdf) return readMap(-1, 4); // map 32 + if (byte >= 0xe0 && byte <= 0xff) return byte - 256; // negative fixint + console.debug("msgpack array:", array); + throw new Error( + "Invalid byte value '" + + byte + + "' at index " + + (pos - 1) + + " in the MessagePack binary data (length " + + array.length + + "): Expecting a range of 0 to 255. This is not a byte array." + ); + } + + function readInt(size) { + let value = 0; + let first = true; + while (size-- > 0) { + if (first) { + let byte = array[pos++]; + value += byte & 0x7f; + if (byte & 0x80) { + value -= 0x80; // Treat most-significant bit as -2^i instead of 2^i + } + first = false; + } else { + value *= 256; + value += array[pos++]; + } + } + return value; + } + + function readUInt(size) { + let value = 0; + while (size-- > 0) { + value *= 256; + value += array[pos++]; + } + return value; + } + + function readFloat(size) { + let view = new DataView(array.buffer, pos + array.byteOffset, size); + pos += size; + if (size === 4) return view.getFloat32(0, false); + if (size === 8) return view.getFloat64(0, false); + } + + function readBin(size, lengthSize) { + if (size < 0) size = readUInt(lengthSize); + let data = array.subarray(pos, pos + size); + pos += size; + return data; + } + + function readMap(size, lengthSize) { + if (size < 0) size = readUInt(lengthSize); + let data = {}; + while (size-- > 0) { + let key = read(); + data[key] = read(); + } + return data; + } + + function readArray(size, lengthSize) { + if (size < 0) size = readUInt(lengthSize); + let data = []; + while (size-- > 0) { + data.push(read()); + } + return data; + } + + function readStr(size, lengthSize) { + if (size < 0) size = readUInt(lengthSize); + let start = pos; + pos += size; + return decodeUtf8(array, start, size); + } + + function readExt(size, lengthSize) { + if (size < 0) size = readUInt(lengthSize); + let type = readUInt(1); + let data = readBin(size); + switch (type) { + case 255: + return readExtDate(data); + } + return { type: type, data: data }; + } + + function readExtDate(data) { + if (data.length === 4) { + let sec = ((data[0] << 24) >>> 0) + ((data[1] << 16) >>> 0) + ((data[2] << 8) >>> 0) + data[3]; + return new Date(sec * 1000); + } + if (data.length === 8) { + let ns = ((data[0] << 22) >>> 0) + ((data[1] << 14) >>> 0) + ((data[2] << 6) >>> 0) + (data[3] >>> 2); + let sec = + (data[3] & 0x3) * pow32 + ((data[4] << 24) >>> 0) + ((data[5] << 16) >>> 0) + ((data[6] << 8) >>> 0) + data[7]; + return new Date(sec * 1000 + ns / 1000000); + } + if (data.length === 12) { + let ns = ((data[0] << 24) >>> 0) + ((data[1] << 16) >>> 0) + ((data[2] << 8) >>> 0) + data[3]; + pos -= 8; + let sec = readInt(8); + return new Date(sec * 1000 + ns / 1000000); + } + throw new Error("Invalid data length for a date value."); + } +} + +// Encodes a string to UTF-8 bytes. +function encodeUtf8(str) { + // Prevent excessive array allocation and slicing for all 7-bit characters + let ascii = true, + length = str.length; + for (let x = 0; x < length; x++) { + if (str.charCodeAt(x) > 127) { + ascii = false; + break; + } + } + + // Based on: https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330 + let i = 0, + bytes = new Uint8Array(str.length * (ascii ? 1 : 4)); + for (let ci = 0; ci !== length; ci++) { + let c = str.charCodeAt(ci); + if (c < 128) { + bytes[i++] = c; + continue; + } + if (c < 2048) { + bytes[i++] = (c >> 6) | 192; + } else { + if (c > 0xd7ff && c < 0xdc00) { + if (++ci >= length) throw new Error("UTF-8 encode: incomplete surrogate pair"); + let c2 = str.charCodeAt(ci); + if (c2 < 0xdc00 || c2 > 0xdfff) + throw new Error( + "UTF-8 encode: second surrogate character 0x" + c2.toString(16) + " at index " + ci + " out of range" + ); + c = 0x10000 + ((c & 0x03ff) << 10) + (c2 & 0x03ff); + bytes[i++] = (c >> 18) | 240; + bytes[i++] = ((c >> 12) & 63) | 128; + } else bytes[i++] = (c >> 12) | 224; + bytes[i++] = ((c >> 6) & 63) | 128; + } + bytes[i++] = (c & 63) | 128; + } + return ascii ? bytes : bytes.subarray(0, i); +} + +// Decodes a string from UTF-8 bytes. +function decodeUtf8(bytes, start, length) { + // Based on: https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330 + let i = start, + str = ""; + length += start; + while (i < length) { + let c = bytes[i++]; + if (c > 127) { + if (c > 191 && c < 224) { + if (i >= length) throw new Error("UTF-8 decode: incomplete 2-byte sequence"); + c = ((c & 31) << 6) | (bytes[i++] & 63); + } else if (c > 223 && c < 240) { + if (i + 1 >= length) throw new Error("UTF-8 decode: incomplete 3-byte sequence"); + c = ((c & 15) << 12) | ((bytes[i++] & 63) << 6) | (bytes[i++] & 63); + } else if (c > 239 && c < 248) { + if (i + 2 >= length) throw new Error("UTF-8 decode: incomplete 4-byte sequence"); + c = ((c & 7) << 18) | ((bytes[i++] & 63) << 12) | ((bytes[i++] & 63) << 6) | (bytes[i++] & 63); + } else throw new Error("UTF-8 decode: unknown multibyte start 0x" + c.toString(16) + " at index " + (i - 1)); + } + if (c <= 0xffff) str += String.fromCharCode(c); + else if (c <= 0x10ffff) { + c -= 0x10000; + str += String.fromCharCode((c >> 10) | 0xd800); + str += String.fromCharCode((c & 0x3ff) | 0xdc00); + } else throw new Error("UTF-8 decode: code point 0x" + c.toString(16) + " exceeds UTF-16 reach"); + } + return str; +} diff --git a/reefcontrols-site/index.js b/reefcontrols-site/index.js index 7ede457..e4d18ad 100644 --- a/reefcontrols-site/index.js +++ b/reefcontrols-site/index.js @@ -9,20 +9,20 @@ import { NT4_Client } from "./NT4.js"; // ***** NETWORKTABLES ***** -const toRobotPrefix = "/ReefControls/ToRobot/"; -const toDashboardPrefix = "/ReefControls/ToDashboard/"; -const selectedLevelTopicName = "SelectedLevel"; -const l1TopicName = "Level1"; -const l2TopicName = "Level2"; -const l3TopicName = "Level3"; -const l4TopicName = "Level4"; -const algaeTopicName = "Algae"; -const coopTopicName = "Coop"; -const isElimsTopicName = "IsElims"; +// const toRobotPrefix = "/ReefControls/ToRobot/"; +const toDashboardPrefix = "/DriveState/"; +// const selectedLevelTopicName = "SelectedLevel"; +// const l1TopicName = "Level1"; +// const l2TopicName = "Level2"; +// const l3TopicName = "Level3"; +// const l4TopicName = "Level4"; +// const algaeTopicName = "Algae"; +// const coopTopicName = "Coop"; +// const isElimsTopicName = "IsElims"; const ntClient = new NT4_Client( window.location.hostname, - "ReefControls", + "Beta2025", () => { // Topic announce }, @@ -31,29 +31,10 @@ const ntClient = new NT4_Client( }, (topic, _, value) => { // New data - if (topic.name === toDashboardPrefix + selectedLevelTopicName) { - selectedLevel = value; - } else if (topic.name === toDashboardPrefix + l1TopicName) { - l1State = value; - } else if (topic.name === toDashboardPrefix + l2TopicName) { - l2State = value; - } else if (topic.name === toDashboardPrefix + l3TopicName) { - l3State = value; - } else if (topic.name === toDashboardPrefix + l4TopicName) { - l4State = value; - } else if (topic.name === toDashboardPrefix + algaeTopicName) { - algaeState = value; - } else if (topic.name === toDashboardPrefix + coopTopicName) { - coopState = value; - } else if (topic.name === toDashboardPrefix + isElimsTopicName) { - isElims = value; - } else { - return; - } - updateUI(); }, () => { // Connected + document.body.style.backgroundColor = "white"; }, () => { // Disconnected @@ -63,29 +44,9 @@ const ntClient = new NT4_Client( // Start NT connection window.addEventListener("load", () => { - ntClient.subscribe( - [ - toDashboardPrefix + selectedLevelTopicName, - toDashboardPrefix + l1TopicName, - toDashboardPrefix + l2TopicName, - toDashboardPrefix + l3TopicName, - toDashboardPrefix + l4TopicName, - toDashboardPrefix + algaeTopicName, - toDashboardPrefix + coopTopicName, - toDashboardPrefix + isElimsTopicName, - ], - false, - false, - 0.02 - ); - ntClient.publishTopic(toRobotPrefix + selectedLevelTopicName, "int"); - ntClient.publishTopic(toRobotPrefix + l1TopicName, "int"); - ntClient.publishTopic(toRobotPrefix + l2TopicName, "int"); - ntClient.publishTopic(toRobotPrefix + l3TopicName, "int"); - ntClient.publishTopic(toRobotPrefix + l4TopicName, "int"); - ntClient.publishTopic(toRobotPrefix + algaeTopicName, "int"); - ntClient.publishTopic(toRobotPrefix + coopTopicName, "boolean"); + ntClient.publishTopic(toDashboardPrefix+"branch", "int"); + ntClient.publishTopic(toDashboardPrefix+"level", "int"); ntClient.connect(); }); @@ -200,6 +161,7 @@ function bind(element, callback) { isTouch = true; } if (isTouch == touchEvent) { + console.log("EEEEEEEEE"); callback(); } }; @@ -227,7 +189,7 @@ window.addEventListener("load", () => { (element, index) => { if (index > 0) { bind(element, () => { - ntClient.addSample(toRobotPrefix + selectedLevelTopicName, index - 1); + selectedLevel = index; }); } } @@ -237,56 +199,47 @@ window.addEventListener("load", () => { Array.from(document.getElementsByClassName("branch")).forEach( (element, index) => { bind(element, () => { - switch (selectedLevel) { - case 0: - ntClient.addSample( - toRobotPrefix + l2TopicName, - l2State ^ (1 << index) - ); - break; - case 1: - ntClient.addSample( - toRobotPrefix + l3TopicName, - l3State ^ (1 << index) - ); - break; - case 2: - ntClient.addSample( - toRobotPrefix + l4TopicName, - l4State ^ (1 << index) - ); - break; - } - }); - } - ); - - // Algae toggle buttons - Array.from(document.getElementsByClassName("algae")).forEach( - (element, index) => { - bind(element, () => { + console.log("FFFFFFF"); + console.log(index); + console.log(selectedLevel); + ntClient.addSample( + toDashboardPrefix+"branch", + index + ); ntClient.addSample( - toRobotPrefix + algaeTopicName, - algaeState ^ (1 << index) + toDashboardPrefix+"level", + selectedLevel ); }); } ); - // L1 count controls - bind(document.getElementsByClassName("subtract")[0], () => { - if (l1State > 0) { - ntClient.addSample(toRobotPrefix + l1TopicName, l1State - 1); - } - }); - bind(document.getElementsByClassName("add")[0], () => { - ntClient.addSample(toRobotPrefix + l1TopicName, l1State + 1); - }); - - // Coop button - bind(document.getElementsByClassName("coop")[0], () => { - ntClient.addSample(toRobotPrefix + coopTopicName, !coopState); - }); + // // Algae toggle buttons + // Array.from(document.getElementsByClassName("algae")).forEach( + // (element, index) => { + // bind(element, () => { + // ntClient.addSample( + // toRobotPrefix + algaeTopicName, + // algaeState ^ (1 << index) + // ); + // }); + // } + // ); + + // // L1 count controls + // bind(document.getElementsByClassName("subtract")[0], () => { + // if (l1State > 0) { + // ntClient.addSample(toRobotPrefix + l1TopicName, l1State - 1); + // } + // }); + // bind(document.getElementsByClassName("add")[0], () => { + // ntClient.addSample(toRobotPrefix + l1TopicName, l1State + 1); + // }); + + // // Coop button + // bind(document.getElementsByClassName("coop")[0], () => { + // ntClient.addSample(toRobotPrefix + coopTopicName, !coopState); + // }); }); // ***** REEF CANVAS ***** diff --git a/simgui-ds.json b/simgui-ds.json index 9f298da..e4167aa 100644 --- a/simgui-ds.json +++ b/simgui-ds.json @@ -1,4 +1,9 @@ { + "Joysticks": { + "window": { + "visible": false + } + }, "Keyboard 0 Settings": { "window": { "visible": true @@ -13,7 +18,7 @@ { "axisConfig": [ { - "decKey": 82, + "decKey": 83, "incKey": 87 }, { @@ -23,22 +28,35 @@ { "decKey": 69, "decayRate": 0.0, + "incKey": 82, "keyRate": 0.009999999776482582 + }, + { + "incKey": 85 + }, + { + "decKey": 263, + "incKey": 262 } ], - "axisCount": 3, - "buttonCount": 4, + "axisCount": 5, + "buttonCount": 15, "buttonKeys": [ 89, - 85, - 73, - 79, -1, -1, -1, -1, -1, - -1 + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 343 ], "povConfig": [ { @@ -57,7 +75,6 @@ { "axisConfig": [ { - "decKey": 74, "incKey": 76 }, { @@ -84,10 +101,7 @@ }, { "axisConfig": [ - { - "decKey": 263, - "incKey": 262 - }, + {}, { "decKey": 265, "incKey": 264 diff --git a/simgui.json b/simgui.json index ebef95f..371d8e5 100644 --- a/simgui.json +++ b/simgui.json @@ -26,7 +26,6 @@ "windows": { "/Pose": { "bottom": 1638, - "builtin": "2025 Reefscape", "height": 8.051901817321777, "left": 534, "right": 3466, @@ -38,7 +37,6 @@ }, "/SmartDashboard/VisionSystemSim-main/Sim Field": { "bottom": 1638, - "builtin": "2025 Reefscape", "height": 8.051901817321777, "left": 534, "right": 3466, @@ -62,8 +60,5 @@ "open": true }, "visible": true - }, - "NetworkTables View": { - "visible": false } } diff --git a/src/main/java/com/team9470/RobotContainer.java b/src/main/java/com/team9470/RobotContainer.java index c3332e6..05d3209 100644 --- a/src/main/java/com/team9470/RobotContainer.java +++ b/src/main/java/com/team9470/RobotContainer.java @@ -7,6 +7,8 @@ import com.team9470.commands.Autos; import com.team9470.subsystems.Superstructure; import com.team9470.subsystems.Swerve; +import com.team9470.subsystems.sim.ButtonBoardPub; +import com.team9470.subsystems.sim.ButtonBoardSub; import com.team9470.subsystems.vision.Vision; import edu.wpi.first.wpilibj.Joystick; import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; @@ -30,7 +32,7 @@ public class RobotContainer { private final SwerveRequest.SwerveDriveBrake brake = new SwerveRequest.SwerveDriveBrake(); private final SwerveRequest.PointWheelsAt point = new SwerveRequest.PointWheelsAt(); public final Swerve drivetrain = TunerConstants.createDrivetrain(); - private final Telemetry logger = new Telemetry(MaxSpeed); + private final Telemetry logger = new Telemetry(); // ---------------- MECHANISM2D -------------------- private final Mechanism2d mech = new Mechanism2d(5, 10); @@ -46,6 +48,9 @@ public class RobotContainer { private final Autos autos = new Autos(superstructure, drivetrain); private final AutoChooser autoChooser = new AutoChooser(); + private final ButtonBoardPub publisher = new ButtonBoardPub(logger); + // private final ButtonBoardSub subscriber = new ButtonBoardSub(logger); + CommandXboxController xbox = new CommandXboxController(0); Joystick buttonBoard = new Joystick(1); @@ -72,11 +77,11 @@ public RobotContainer() { public void periodic(){ drivetrain.periodic(); - + // subscriber.periodic(); + // autoScoring } private void configureBindings() { - drivetrain.registerTelemetry(logger::telemeterize); drivetrain.setDefaultCommand( drivetrain.applyRequest(() -> @@ -106,15 +111,15 @@ private void configureBindings() { final int id = i; Trigger trig = new Trigger(() -> (id < 2) ? buttonBoard.getX() == Math.pow(-1.0, id + 1) : buttonBoard.getY() == Math.pow(-1.0, id)); - trig.whileTrue(new InstantCommand(() -> autoScoring.setLevel(id + 1))); + trig.whileTrue(new InstantCommand(() -> autoScoring.overrideLevel(id + 1))); } // Reef position bindings (remaining unchanged) - for (int i = 0; i < 12; i++) { - JoystickButton button = new JoystickButton(buttonBoard, i+1); - final int id = i; - button.whileTrue(new InstantCommand(() -> autoScoring.setBranch(id))); - } + // for (int i = 0; i < 12; i++) { + // JoystickButton button = new JoystickButton(buttonBoard, i+1); + // final int id = i; + // button.whileTrue(new InstantCommand(() -> autoScoring.setBranch(id))); + // } xbox.rightTrigger() .whileTrue(autoScoring.autoScore(superstructure)) @@ -124,6 +129,6 @@ private void configureBindings() { xbox.y() .whileTrue(autoScoring.autoScoreNoDrive(superstructure).onlyIf(superstructure.getCoral()::hasCoral)); - xbox.rightStick().whileTrue(new InstantCommand(autoScoring::updateClosestReefPos)); + // xbox.rightStick().whileTrue(new InstantCommand(autoScoring::updateClosestReefPos)); } } \ No newline at end of file diff --git a/src/main/java/com/team9470/Telemetry.java b/src/main/java/com/team9470/Telemetry.java index 4dd9686..4118f29 100644 --- a/src/main/java/com/team9470/Telemetry.java +++ b/src/main/java/com/team9470/Telemetry.java @@ -1,124 +1,19 @@ package com.team9470; -import com.ctre.phoenix6.SignalLogger; -import com.ctre.phoenix6.swerve.SwerveDrivetrain.SwerveDriveState; - -import edu.wpi.first.math.geometry.Pose2d; -import edu.wpi.first.math.kinematics.ChassisSpeeds; -import edu.wpi.first.math.kinematics.SwerveModulePosition; -import edu.wpi.first.math.kinematics.SwerveModuleState; -import edu.wpi.first.networktables.DoubleArrayPublisher; -import edu.wpi.first.networktables.DoublePublisher; +import edu.wpi.first.networktables.IntegerPublisher; import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.NetworkTableInstance; -import edu.wpi.first.networktables.StringPublisher; -import edu.wpi.first.networktables.StructArrayPublisher; -import edu.wpi.first.networktables.StructPublisher; -import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d; -import edu.wpi.first.wpilibj.smartdashboard.MechanismLigament2d; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; -import edu.wpi.first.wpilibj.util.Color; -import edu.wpi.first.wpilibj.util.Color8Bit; public class Telemetry { - private final double MaxSpeed; - - /** - * Construct a telemetry object, with the specified max speed of the robot - * - * @param maxSpeed Maximum speed in meters per second - */ - public Telemetry(double maxSpeed) { - MaxSpeed = maxSpeed; - SignalLogger.start(); - } - /* What to publish over networktables for telemetry */ private final NetworkTableInstance inst = NetworkTableInstance.getDefault(); /* Robot swerve drive state */ private final NetworkTable driveStateTable = inst.getTable("DriveState"); - private final StructPublisher drivePose = driveStateTable.getStructTopic("Pose", Pose2d.struct).publish(); - private final StructPublisher driveSpeeds = driveStateTable.getStructTopic("Speeds", ChassisSpeeds.struct).publish(); - private final StructArrayPublisher driveModuleStates = driveStateTable.getStructArrayTopic("ModuleStates", SwerveModuleState.struct).publish(); - private final StructArrayPublisher driveModuleTargets = driveStateTable.getStructArrayTopic("ModuleTargets", SwerveModuleState.struct).publish(); - private final StructArrayPublisher driveModulePositions = driveStateTable.getStructArrayTopic("ModulePositions", SwerveModulePosition.struct).publish(); - private final DoublePublisher driveTimestamp = driveStateTable.getDoubleTopic("Timestamp").publish(); - private final DoublePublisher driveOdometryFrequency = driveStateTable.getDoubleTopic("OdometryFrequency").publish(); - - /* Robot pose for field positioning */ - private final NetworkTable table = inst.getTable("Pose"); - private final DoubleArrayPublisher fieldPub = table.getDoubleArrayTopic("robotPose").publish(); - private final StringPublisher fieldTypePub = table.getStringTopic(".type").publish(); - - /* Mechanisms to represent the swerve module states */ - private final Mechanism2d[] m_moduleMechanisms = new Mechanism2d[] { - new Mechanism2d(1, 1), - new Mechanism2d(1, 1), - new Mechanism2d(1, 1), - new Mechanism2d(1, 1), - }; - /* A direction and length changing ligament for speed representation */ - private final MechanismLigament2d[] m_moduleSpeeds = new MechanismLigament2d[] { - m_moduleMechanisms[0].getRoot("RootSpeed", 0.5, 0.5).append(new MechanismLigament2d("Speed", 0.5, 0)), - m_moduleMechanisms[1].getRoot("RootSpeed", 0.5, 0.5).append(new MechanismLigament2d("Speed", 0.5, 0)), - m_moduleMechanisms[2].getRoot("RootSpeed", 0.5, 0.5).append(new MechanismLigament2d("Speed", 0.5, 0)), - m_moduleMechanisms[3].getRoot("RootSpeed", 0.5, 0.5).append(new MechanismLigament2d("Speed", 0.5, 0)), - }; - /* A direction changing and length constant ligament for module direction */ - private final MechanismLigament2d[] m_moduleDirections = new MechanismLigament2d[] { - m_moduleMechanisms[0].getRoot("RootDirection", 0.5, 0.5) - .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), - m_moduleMechanisms[1].getRoot("RootDirection", 0.5, 0.5) - .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), - m_moduleMechanisms[2].getRoot("RootDirection", 0.5, 0.5) - .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), - m_moduleMechanisms[3].getRoot("RootDirection", 0.5, 0.5) - .append(new MechanismLigament2d("Direction", 0.1, 0, 0, new Color8Bit(Color.kWhite))), - }; - - private final double[] m_poseArray = new double[3]; - private final double[] m_moduleStatesArray = new double[8]; - private final double[] m_moduleTargetsArray = new double[8]; + /* For button board communications */ + // public final StringPublisher coralInfo = driveStateTable.getStringTopic("CoralInfo").publish(); + public final IntegerPublisher branch = driveStateTable.getIntegerTopic("branch").publish(); + public final IntegerPublisher level = driveStateTable.getIntegerTopic("level").publish(); /** Accept the swerve drive state and telemeterize it to SmartDashboard and SignalLogger. */ - public void telemeterize(SwerveDriveState state) { - /* Telemeterize the swerve drive state */ - drivePose.set(state.Pose); - driveSpeeds.set(state.Speeds); - driveModuleStates.set(state.ModuleStates); - driveModuleTargets.set(state.ModuleTargets); - driveModulePositions.set(state.ModulePositions); - driveTimestamp.set(state.Timestamp); - driveOdometryFrequency.set(1.0 / state.OdometryPeriod); - - /* Also write to log file */ - m_poseArray[0] = state.Pose.getX(); - m_poseArray[1] = state.Pose.getY(); - m_poseArray[2] = state.Pose.getRotation().getDegrees(); - for (int i = 0; i < 4; ++i) { - m_moduleStatesArray[i*2 + 0] = state.ModuleStates[i].angle.getRadians(); - m_moduleStatesArray[i*2 + 1] = state.ModuleStates[i].speedMetersPerSecond; - m_moduleTargetsArray[i*2 + 0] = state.ModuleTargets[i].angle.getRadians(); - m_moduleTargetsArray[i*2 + 1] = state.ModuleTargets[i].speedMetersPerSecond; - } - - SignalLogger.writeDoubleArray("DriveState/Pose", m_poseArray); - SignalLogger.writeDoubleArray("DriveState/ModuleStates", m_moduleStatesArray); - SignalLogger.writeDoubleArray("DriveState/ModuleTargets", m_moduleTargetsArray); - SignalLogger.writeDouble("DriveState/OdometryPeriod", state.OdometryPeriod, "seconds"); - - /* Telemeterize the pose to a Field2d */ - fieldTypePub.set("Field2d"); - fieldPub.set(m_poseArray); - - /* Telemeterize the module states to a Mechanism2d */ - for (int i = 0; i < 4; ++i) { - m_moduleSpeeds[i].setAngle(state.ModuleStates[i].angle); - m_moduleDirections[i].setAngle(state.ModuleStates[i].angle); - m_moduleSpeeds[i].setLength(state.ModuleStates[i].speedMetersPerSecond / (2 * MaxSpeed)); - - SmartDashboard.putData("Module " + i, m_moduleMechanisms[i]); - } - } } diff --git a/src/main/java/com/team9470/commands/AutoScoring.java b/src/main/java/com/team9470/commands/AutoScoring.java index 3bfa6c6..dc01d50 100644 --- a/src/main/java/com/team9470/commands/AutoScoring.java +++ b/src/main/java/com/team9470/commands/AutoScoring.java @@ -15,21 +15,21 @@ import static edu.wpi.first.units.Units.Meters; public class AutoScoring { - - private CoralObjective coralObjective = AutoScoring.CoralObjective.NONE; - private final Swerve drivetrain; + private ScoringState scoringState = ScoringState.DEFAULT; + private static Swerve drivetrain; public AutoScoring(Swerve drivetrain){ - this.drivetrain = drivetrain; + AutoScoring.drivetrain = drivetrain; } public Command autoScore(Superstructure superstructure) { return new DeferredCommand(() -> { + CoralObjective coralObjective = scoringState.getOptimalObjective(); Command driveToScore = new DriveToPose(coralObjective::getScoringPose, drivetrain) .alongWith( new WaitUntilCommand(() -> closeEnough(coralObjective, Constants.DriverAssistConstants.RAISE_DISTANCE)) .andThen(superstructure.waitForIntake().asProxy()) - .andThen(superstructure.raise(coralObjective.level).asProxy()) + .andThen(superstructure.raise(scoringState.getLevel()).asProxy()) ); System.out.println("Scoring with coralobjective: " + coralObjective); return driveToScore.andThen(superstructure.getCoral().scoreCommand().asProxy()); @@ -59,7 +59,7 @@ public static Command autoScoreWithTimeout(Superstructure superstructure, CoralO return driveToScore.andThen(superstructure.getCoral().scoreCommand().withTimeout(timeout).asProxy()); } public Command autoScoreNoDrive(Superstructure superstructure) { - return new DeferredCommand(() -> superstructure.raise(coralObjective.level), Set.of(superstructure)).andThen(superstructure.score()); + return new DeferredCommand(() -> superstructure.raise(scoringState.getLevel()), Set.of(superstructure)).andThen(superstructure.score()); } private Pose2d computeDriveBackPose(Pose2d scoringPose) { @@ -69,7 +69,7 @@ private Pose2d computeDriveBackPose(Pose2d scoringPose) { } public Command driveBack(){ - Pose2d scoringPose = coralObjective.getScoringPose(); + Pose2d scoringPose = scoringState.getOptimalObjective().getScoringPose(); Pose2d driveBackPose = computeDriveBackPose(scoringPose); return new DriveToPose(() -> driveBackPose, drivetrain); @@ -77,13 +77,14 @@ public Command driveBack(){ } public Command autoAlgae(Superstructure superstructure){ - return new DriveToPose(() -> coralObjective.getScoringPose(), drivetrain) + CoralObjective coralObjective = scoringState.getOptimalObjective(); + return new DriveToPose(coralObjective::getScoringPose, drivetrain) .alongWith(new WaitUntilCommand(() -> closeEnough(coralObjective, Constants.DriverAssistConstants.RAISE_DISTANCE))) .andThen(new DeferredCommand(() -> superstructure.dealgify(coralObjective.getAlgaeLevel()), Set.of(superstructure))); } public Command autoAlgaeNoDrive(Superstructure superstructure){ - return new DeferredCommand(() -> superstructure.dealgify(coralObjective.getAlgaeLevel()), Set.of(superstructure)); + return new DeferredCommand(() -> superstructure.dealgify(scoringState.getOptimalObjective().getAlgaeLevel()), Set.of(superstructure)); } private static boolean closeEnough(CoralObjective objective, Distance distance){ @@ -92,36 +93,81 @@ private static boolean closeEnough(CoralObjective objective, Distance distance){ return currentPose.getTranslation().getDistance(scoringPose.getTranslation()) < distance.in(Meters); } - // Update the reef value used for automatic level selection. - public void setBranch(int branchId) { - coralObjective = coralObjective.updateBranchId(branchId); - System.out.println("Branch ID: " + branchId); + + // allows op to choose level + public void overrideLevel(int level) { + scoringState = scoringState.updateLevel(level); } - public void setLevel(int level) { - coralObjective = coralObjective.updateLevel(level); + // removes operator override of level + public void removeOverride(){ + scoringState = scoringState.updateLevel(0); } - public void updateClosestReefPos() { - Pose2d[] reefPoses = Constants.DriverAssistConstants.getReefPositions(); - // Get the current robot pose at initialization. - Pose2d currentPose = Swerve.getInstance().getPose(); + public void addCoral(int branchId, int level){ + scoringState = scoringState.updateCoral(branchId, level, true); + } + + public void removeCoral(int branchId, int level){ + scoringState = scoringState.updateCoral(branchId, level, false); + } + + public boolean[][] getCorals(){ + return scoringState.corals; + } + + // stores scored coral and current level + // if curLevel = 0, it will automatically set it + public record ScoringState(boolean[][] corals, int curLevel){ + public static final ScoringState DEFAULT = new ScoringState(new boolean[4][12], 0); + + public ScoringState updateCoral(int branchId, int level, boolean newValue){ + corals[level-1][branchId] = newValue; + return new ScoringState(corals, curLevel); + } - // Find the closest reef pose. - double shortestDistance = Double.MAX_VALUE; - int closestPoseId = -1; - for (int i = 0; i < 12; i++) { - Pose2d pose = reefPoses[i]; - double distance = currentPose.getTranslation().getDistance(pose.getTranslation()); - if (distance < shortestDistance) { - shortestDistance = distance; - closestPoseId = i; + public int getLevel(){ + if(curLevel == 0){ + for(int i = 3; i >= 0 ; i--){ + for(int j = 0; j < 12; j++){ + if(!corals[i][j]){ + return i+1; + } + } + } + return 1; } + return curLevel; } - setBranch(closestPoseId); - } + public ScoringState updateLevel(int newLevel){ + return new ScoringState(corals, newLevel); + } + public CoralObjective getOptimalObjective(){ + Pose2d[] reefPoses = Constants.DriverAssistConstants.getReefPositions(); + Pose2d currentPose = drivetrain.getPose(); + int level = getLevel(); + + // Find the closest reef pose. + double shortestDistance = Double.MAX_VALUE; + int closestPoseId = -1; + for (int i = 0; i < 12; i++) { + // if coral is already scored here, can't select! + if (corals[level-1][i]) { + continue; + } + Pose2d pose = reefPoses[i]; + double distance = currentPose.getTranslation().getDistance(pose.getTranslation()); + if (distance < shortestDistance) { + shortestDistance = distance; + closestPoseId = i; + } + } + return new CoralObjective(closestPoseId, level); + } + } + // Starting facing the driver station, moving counterclockwise public record CoralObjective(int branchId, int level){ public static final CoralObjective NONE = new CoralObjective(0, 0); @@ -139,13 +185,5 @@ public int getAlgaeLevel(){ // starting with (0, 1) = 3, then (2, 3) = 2, each reef alternates level between 2 and 3 return (branchId)/2 % 2 == 0 ? 3 : 2; } - - public CoralObjective updateBranchId(int branchId){ - return new CoralObjective(branchId, level); - } - - public CoralObjective updateLevel(int level){ - return new CoralObjective(branchId, level); - } } } diff --git a/src/main/java/com/team9470/subsystems/sim/ButtonBoardPub.java b/src/main/java/com/team9470/subsystems/sim/ButtonBoardPub.java new file mode 100644 index 0000000..ba009ae --- /dev/null +++ b/src/main/java/com/team9470/subsystems/sim/ButtonBoardPub.java @@ -0,0 +1,61 @@ +package com.team9470.subsystems.sim; + +import com.team9470.Telemetry; +import com.team9470.util.Util; +import edu.wpi.first.wpilibj.Joystick; +import edu.wpi.first.wpilibj2.command.SubsystemBase; +import com.google.gson.Gson; +import edu.wpi.first.wpilibj2.command.button.JoystickButton; + +public class ButtonBoardPub extends SubsystemBase { + private static ButtonBoardPub mInstance; + // Joystick buttonBoard; + private final boolean[][] corals; + private final Telemetry logger; + public Gson gson = new Gson(); + + public static ButtonBoardPub getInstance(Telemetry logger) { + if (mInstance == null) { + mInstance = new ButtonBoardPub(logger); + } + + return mInstance; + } + + public ButtonBoardPub(Telemetry logger) { + this.logger = logger; + + // buttonBoard = new Joystick(1); + + // TODO: will actually be populated by website! + corals = new boolean[4][12]; + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 12; j++) { + corals[i][j] = false; + } + } + + // branchId = 2 (NO SUBTRACT), level = 4 (- 1) + // corals[0][2] = true; + // corals[1][2] = true; + // corals[2][2] = true; + // corals[3][2] = true; + } + + @Override + public void periodic() { +// int opBranchId = 0; +// int level = 0; + +// corals[level][opBranchId] = true; +// // buildAutoChooser(String defaultAutoName, return chooser; } + +// /* converts a 2D boolean array to JSON to be sent across a string */ +// public static String Boolean2DArrayToJSON(boolean[][] data) { +// JSONArray jsonArray = new JSONArray(); +// +// for (boolean[] row : data) { +// jsonArray.put(new JSONArray(row)); +// } +// +// return jsonArray.toString(); +// } +// +// /* de-serializes JSONArray (String form) to a 2D boolean array */ +// public static boolean[][] BooleanJSONArrayStringTo2DArray(String jsonArrayString) { +// JSONArray jsonArray = new JSONArray(jsonArrayString); +// +// boolean[][] result = new boolean[12][3]; +// for (int i = 0; i < jsonArray.length(); i++) { +// JSONArray row = jsonArray.getJSONArray(i); +// result[i] = new boolean[row.length()]; +// for (int j = 0; j < row.length(); j++) { +// result[i][j] = row.getBoolean(j); +// } +// } +// +// return result; +// } }