diff --git a/.vale.ini b/.vale.ini index 777427cf..59f36b57 100644 --- a/.vale.ini +++ b/.vale.ini @@ -4,8 +4,26 @@ StylesPath = .styles MinAlertLevel = warning Packages = Google, write-good +# Frcsoftware.Spelling (below) is our spellcheck rule; it replaces Vale's +# built-in Vale.Spelling so we control the dictionary in use. The 'Vale' +# style itself must stay in BasedOnStyles since it's what makes the Vocab +# below (and Vale.Terms/Vale.Avoid) take effect. +# +# To allow a word through spellcheck, or to enforce that a term always +# appears with a specific casing (e.g. "WPILib", not "wpilib"), add it to +# vale-accept-words.txt or src/data/glossary.ts (see setup-vale.ts). +Vocab = Frcsoftware + [*.mdx] -BasedOnStyles = Google, write-good +BasedOnStyles = Vale, Google, write-good, Frcsoftware + +# "Disabled" and "enabled" are the names of the robot's states in FRC (they're +# what the Driver Station's Enable/Disable buttons put the robot into), so +# Google.WordListCase's "use 'turn off' instead" advice doesn't apply to them. +TokenIgnores = (?i)\b(?:disabled|enabled)\b + +Vale.Spelling = NO +Frcsoftware.Spelling = YES # Google style - selectively enable useful rules Google.GenderBias = error diff --git a/scripts/setup-vale.ts b/scripts/setup-vale.ts index f496e64f..312c681c 100644 --- a/scripts/setup-vale.ts +++ b/scripts/setup-vale.ts @@ -1,28 +1,28 @@ -import { createWriteStream, writeFileSync, existsSync, mkdirSync } from 'fs'; -import { resolve, dirname } from 'path'; +import { + createWriteStream, + writeFileSync, + readFileSync, + copyFileSync, + existsSync, + mkdirSync, +} from 'fs'; +import { resolve } from 'path'; import { fileURLToPath } from 'url'; import { glossaryTerms } from '../src/data/glossary'; import { pipeline } from 'stream/promises'; -// Update glossary terms const ROOT = fileURLToPath(new URL('..', import.meta.url)); -const OUTPUT = resolve(ROOT, '.styles/config/ignore/glossary.txt'); -const OUTPUT_DIR = dirname(OUTPUT); -if (!existsSync(OUTPUT_DIR)) { - mkdirSync(OUTPUT_DIR, { recursive: true }); -} - -const terms = [...new Set(glossaryTerms.map(({ term }) => term))].sort((a, b) => - a.toLowerCase().localeCompare(b.toLowerCase()), -); +const DICT_DIR = resolve(ROOT, '.styles/config/dictionaries'); +const DIC_PATH = resolve(DICT_DIR, 'en_US.dic'); +const AFF_PATH = resolve(DICT_DIR, 'en_US.aff'); -const content = terms.join('\n') + '\n'; -writeFileSync(OUTPUT, content); +const ACCEPT_WORDS_PATH = resolve(ROOT, 'vale-accept-words.txt'); +const SPELLING_RULE_PATH = resolve(ROOT, 'scripts/vale-spelling-rule.yml'); -console.log(`Wrote ${terms.length} glossary terms to ${OUTPUT}.`); +const VOCAB_DIR = resolve(ROOT, '.styles/config/vocabularies/Frcsoftware'); +const STYLE_DIR = resolve(ROOT, '.styles/Frcsoftware'); -// If not already present, download dictionary async function downloadFile(url: string, path: string) { const response = await fetch(url); if (!response.ok) @@ -38,20 +38,81 @@ async function downloadFile(url: string, path: string) { await pipeline(response.body, fileStream); } -const dictsToDownload = [ - { - path: resolve(ROOT, '.styles/config/dictionaries/en_US.dic'), - url: 'https://raw.githubusercontent.com/LibreOffice/dictionaries/refs/tags/libreoffice-26.2.5.1/en/en_US.dic', - }, - { - path: resolve(ROOT, '.styles/config/dictionaries/en_US.aff'), - url: 'https://raw.githubusercontent.com/LibreOffice/dictionaries/refs/tags/libreoffice-26.2.5.1/en/en_US.aff', - }, -]; - -dictsToDownload.forEach((dict) => { - if (!existsSync(dict.path)) { - mkdirSync(dirname(dict.path), { recursive: true }); - downloadFile(dict.url, dict.path); +// Vale's spelling check needs a Hunspell dictionary; we pin a specific +// LibreOffice release rather than relying on whatever Vale bundles. +async function ensureDictionaries() { + if (existsSync(DIC_PATH) && existsSync(AFF_PATH)) return; + + mkdirSync(DICT_DIR, { recursive: true }); + const version = 'libreoffice-26.2.5.1'; + await Promise.all([ + downloadFile( + `https://raw.githubusercontent.com/LibreOffice/dictionaries/refs/tags/${version}/en/en_US.dic`, + DIC_PATH, + ), + downloadFile( + `https://raw.githubusercontent.com/LibreOffice/dictionaries/refs/tags/${version}/en/en_US.aff`, + AFF_PATH, + ), + ]); +} + +// Hunspell .dic files are "\n/\n...". +function loadDictionaryWords(): Set { + const [, ...lines] = readFileSync(DIC_PATH, 'utf-8').split('\n'); + const words = new Set(); + for (const line of lines) { + const word = (line.split('/')[0] ?? '').trim(); + if (word) words.add(word.toLowerCase()); } -}); + return words; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// Builds Vale's Vocab accept list (`.styles/config/vocabularies/Frcsoftware/accept.txt`) +// from two committed sources: +// - vale-accept-words.txt: plain jargon words, always accepted regardless of casing. +// - src/data/glossary.ts: terms with tooltip definitions. A term marked +// `caseSensitive: true` is enforced with its exact casing (e.g. "WPILib"), +// UNLESS its lowercase form is itself a real English word (e.g. "CAN"), +// in which case enforcing casing would flag ordinary prose ("can you...") +// as an error, so it falls back to case-insensitive acceptance. +function buildAcceptEntries(dictionaryWords: Set): string[] { + const entries = new Set(); + + for (const { term, caseSensitive } of glossaryTerms) { + const enforceCase = + caseSensitive && !dictionaryWords.has(term.toLowerCase()); + entries.add( + enforceCase ? escapeRegExp(term) : `(?i)${escapeRegExp(term)}`, + ); + } + + const customWords = readFileSync(ACCEPT_WORDS_PATH, 'utf-8') + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')); + for (const word of customWords) { + entries.add(`(?i)${escapeRegExp(word)}`); + } + + return [...entries].sort((a, b) => + a.toLowerCase().localeCompare(b.toLowerCase()), + ); +} + +await ensureDictionaries(); + +const acceptEntries = buildAcceptEntries(loadDictionaryWords()); +mkdirSync(VOCAB_DIR, { recursive: true }); +writeFileSync( + resolve(VOCAB_DIR, 'accept.txt'), + acceptEntries.join('\n') + '\n', +); +console.log(`Wrote ${acceptEntries.length} accepted terms to Vale vocabulary.`); + +mkdirSync(STYLE_DIR, { recursive: true }); +copyFileSync(SPELLING_RULE_PATH, resolve(STYLE_DIR, 'Spelling.yml')); diff --git a/scripts/vale-spelling-rule.yml b/scripts/vale-spelling-rule.yml new file mode 100644 index 00000000..5a28cfd9 --- /dev/null +++ b/scripts/vale-spelling-rule.yml @@ -0,0 +1,5 @@ +extends: spelling +message: "Did you really mean '%s'?" +level: warning +dictionaries: + - en_US diff --git a/src/content/docs/best-practices/git-usage.mdx b/src/content/docs/best-practices/git-usage.mdx index ccb17e53..6379a9a9 100644 --- a/src/content/docs/best-practices/git-usage.mdx +++ b/src/content/docs/best-practices/git-usage.mdx @@ -35,7 +35,7 @@ For advanced users, the command line offers more capabilities. The `main` branch is where the working, tested version of the code lives during the build season. When multiple programmers are working on different changes, creating separate development branches can help prevent merge conflicts. -For example, creating a seprate branch for vision code ensures that the code on the drivetrain branch isn't affected. +For example, creating a separate branch for vision code ensures that the code on the drivetrain branch isn't affected. Making branches for each competition helps isolate fixes and ensures that code is still reviewed before merging to `main`. To maintain a branch, you must stay up to date with `main`. diff --git a/src/content/docs/contribution/methodsOfContributing.mdx b/src/content/docs/contribution/methodsOfContributing.mdx index 83b13f0c..a422eebc 100644 --- a/src/content/docs/contribution/methodsOfContributing.mdx +++ b/src/content/docs/contribution/methodsOfContributing.mdx @@ -89,7 +89,7 @@ You can get a local hosted version of the website to have a live preview of the 1. Open the repository in VS Code (doesn't matter which branch) 2. Toggle the bottom panel on if there isn't one with the shortcut `Ctrl + J` -3. Click the dropdown next to the + on the top righthand side of the bottom panel and click "Terminal" +3. Click the dropdown next to the + on the top right-hand side of the bottom panel and click "Terminal" 4. Run the command `pnpm install` to install all needed packages (FIRST TIME) 5. Run the command `pnpm dev` to start the development server 6. If everything went smoothly it should say its serving on something like `http://localhost:4321` diff --git a/src/content/docs/learning-course/getting-started/required-tools.mdx b/src/content/docs/learning-course/getting-started/required-tools.mdx index 9197edd8..4a8161c7 100644 --- a/src/content/docs/learning-course/getting-started/required-tools.mdx +++ b/src/content/docs/learning-course/getting-started/required-tools.mdx @@ -22,7 +22,7 @@ You can find the download link [here](https://github.com/wpilibsuite/allwpilib/r tools and not 2026 WPILib tools. -Once you have downloaded the 2027 WPILib tools, you can follow the instructions on how to set up the WPILib tools here [here](https://docs.wpilib.org/en/stable/docs/zero-to-robot/step-2/wpilib-setup.html). +Once you have downloaded the 2027 WPILib tools, you can follow the instructions on how to set up the WPILib tools [here](https://docs.wpilib.org/en/stable/docs/zero-to-robot/step-2/wpilib-setup.html). The WPILib tool package also includes different programs that are useful for data logging, simulation, dashboards, and more. We will use some of these tools in later stages. diff --git a/src/content/docs/learning-course/getting-started/vscode-overview.mdx b/src/content/docs/learning-course/getting-started/vscode-overview.mdx index cf1b3ef9..3e93a44f 100644 --- a/src/content/docs/learning-course/getting-started/vscode-overview.mdx +++ b/src/content/docs/learning-course/getting-started/vscode-overview.mdx @@ -39,7 +39,7 @@ VS Code's layout is categorized into a few key regions: The editor is star of the show and it is where you can view and edit files. - Next, the **Panel** is at the bottom of the screen. It has four main views, which are the Terminal, Problems, Output, and Debug Console. -- Finally, the **Command Palette** is accesed through `Ctrl+Shift+P` / `Cmd+Shift+P` and it allows for you to search and run any command in VS Code. +- Finally, the **Command Palette** is accessed through `Ctrl+Shift+P` / `Cmd+Shift+P` and it allows for you to search and run any command in VS Code. When the controller's A button is pressed the IntakeLauncher motor should have a throttle of -0.8 while the Feeder motor should have a throttle of 1.0. -This will outake the fuel. +This will outtake the fuel. @@ -249,7 +249,7 @@ This will stop the motors. The process for testing the new motors in teleop is very similar to the process used to test the drivetrain. Holding the "E" key will cause the robot to intake, "Q" will cause the robot to launch. -and "R" will cause the robot to outake. +and "R" will cause the robot to outtake. Instead of looking at the 2D Field tab in Advantage Scope, use the Line Graph tab to view data from the motors. The voltage applied from the motor controllers to their motors can be viewed on the left axis while the right axis shows the motors' velocities. diff --git a/src/content/docs/learning-course/stage1/stage1a/kitbot-drivetrain.mdx b/src/content/docs/learning-course/stage1/stage1a/kitbot-drivetrain.mdx index 9f837fce..b47df847 100644 --- a/src/content/docs/learning-course/stage1/stage1a/kitbot-drivetrain.mdx +++ b/src/content/docs/learning-course/stage1/stage1a/kitbot-drivetrain.mdx @@ -39,7 +39,7 @@ When creating a motor controller object, the physical motor controller's CAN ID CAN Bus refers to which of the 5 Systemcore CAN ports, or which CANivore, the device is plugged into. CAN ID is an integer that each CAN device is configured to have. All devices on a given CAN Bus must have a unique ID. -Using the combination of CAN Bus and CAN ID SystemCore can give commands to the correct motor controller. +Using the combination of CAN Bus and CAN ID Systemcore can give commands to the correct motor controller. For this exercise the motor controllers will have the IDs: @@ -121,7 +121,7 @@ the configurations that need to be added and their proper values. For this section only the motor controller's invert setting will be configured. This setting controls what direction a motor spins when the motor controller is given a command with a positive sign. Since there are two motors on each side of the drivetrain, its important to ensure that the each of the motors on a side -move in sync with eachother. +move in sync with each other. This can be accomplished by telling one of the motor controllers to follow the other. This is why one motor is named Leader and the other is Follower. The code tells the Follower to listen to the commands given to the Leader. @@ -225,7 +225,7 @@ Remember that some of the configurations may be different. While there are several ways to control a tank drive, this stage will be using arcade drive. Arcade drive uses the y-axis of a joystick to control how fast the robot drives forward or backward while the x-axis controls how fast the robot rotates clockwise or counter clockwise. -WPIlib provides a class to convert joystick inputs into commands for the motors to follow called `DifferentialDrive`. +WPILib provides a class to convert joystick inputs into commands for the motors to follow called `DifferentialDrive`. An instance of `DifferentialDrive` should be created under where the motor controllers were declared. @@ -316,10 +316,10 @@ By now your `Robot` class in `Robot.java` should look like this and VS code shou # OpModes -`OpMode`s are a class that registers itself with the driverstation providing a name and robot mode (autonomous, teleop, or utility). -This allows the robot to run different code based on what is selected on the driverstation. +`OpMode`s are a class that registers itself with the Driver Station providing a name and robot mode (autonomous, teleop, or utility). +This allows the robot to run different code based on what is selected on the Driver Station. This stage will use classes that extend `PeriodicOpMode.` -By extending `PeriodicOpMode` these classes gain a few useful functions that are only called when the OpMode is selected on the driverstation. +By extending `PeriodicOpMode` these classes gain a few useful functions that are only called when the OpMode is selected on the Driver Station. - `start()` is called once when the robot transitions from disabled to enabled. - `periodic()` is called repeatedly when the robot is enabled. @@ -332,7 +332,7 @@ Two blank `PeriodicOpMode`s, `MyTeleop.java` and `MyAuto.java` are provided unde To control the robot with joysticks a Teleop OpMode needs to be created that periodically gives the `DifferentialDrive` instance new values from the controller. First a instance of `NiDsXboxController` needs to be created. This class has functions that provide the state of different buttons on the controller. -Multiple controllers can be used at once so the driverstation gives each a slot. +Multiple controllers can be used at once so the Driver Station gives each a slot. The index provided in the constructor tells the `NiDsXboxController`which slot to listen too. diff --git a/src/content/docs/learning-course/stage1/stage1a/simple-auto.mdx b/src/content/docs/learning-course/stage1/stage1a/simple-auto.mdx index 8e22c46d..6c52ab52 100644 --- a/src/content/docs/learning-course/stage1/stage1a/simple-auto.mdx +++ b/src/content/docs/learning-course/stage1/stage1a/simple-auto.mdx @@ -21,8 +21,8 @@ Additionally both game pieces and your robot are in a known position allowing fo You also need to coordinate your autonomous routine with your alliance partners so it is important to have multiple routines and the ability to adjust them. An autonomous routine is simply an OpMode that gets run during the autonomous portion of the match. -When writing an OpMode that will be run during the autonomous mode, there is an `@Autonomous` annotation that is added above the class definition. -This annotation tells the driverstation to place the OpMode under the autonomous selector. +When writing an OpMode that will be run during the autonomous mode, there is an `@Autonomous` annotation that is added directly before the class definition. +This annotation tells the Driver Station to place the OpMode under the autonomous selector. This OpMode will use a `Timer` to base its behavior off of the time elapsed from the beginning of the autonomous period. Time-based autos trade off simplicity for reliability, since slight variations in starting configuration could cause your robot to take more or less time to perform different actions diff --git a/src/content/docs/learning-course/stage1/stage1b/command-based-overview.mdx b/src/content/docs/learning-course/stage1/stage1b/command-based-overview.mdx index 6c75ad3d..07e13b42 100644 --- a/src/content/docs/learning-course/stage1/stage1b/command-based-overview.mdx +++ b/src/content/docs/learning-course/stage1/stage1b/command-based-overview.mdx @@ -15,9 +15,10 @@ set of instructions (execute this task, then this, and finally this). However, robots (and humans!) don't just execute a set of tasks and shut down. Imagine your daily routine. -When your alarm clock rings, you turn it off, then tumble out of bed. +When an alarm clock rings, you turn it off, then tumble out of bed. When your belly rumbles, you walk to the fridge, then get a snack. -When the clock strikes 8 AM, you open the front door to leave for school. +When the clock strikes 8 AM, +you open the front door to leave for school. @@ -64,7 +65,7 @@ the mechanisms they require - thus, mechanisms are also called "requirements". An intake is a mechanism because your code controls its speed. On the other hand, -an apriltag camera wouldn't be because your code only reads data from it, but doesn't update its state. +an AprilTag camera wouldn't be because your code only reads data from it, but doesn't update its state.