Skip to content

Commit

Permalink
Allow project settings to be configurable (#31)
Browse files Browse the repository at this point in the history
* Add basic project settings

Uses team number setting in generated preferences file

* Include project json file in project export

* Add a UI for configuring settings

* Respect epilogue support setting when generating code

* Show settings when creating a new project

* Default new project names to empty string

* Move project name to settings object for consistency

* Validate settings to prevent saving with bad input (eg no project name or team number)

* Trim leading and trailing whitespace from text on save

* Make settings flexible

* Add definitions for grouping settings together

* Allow custom settings to be defined (eg by plugins)
  • Loading branch information
SamCarlberg authored Sep 28, 2024
1 parent 4127315 commit e5f1199
Show file tree
Hide file tree
Showing 13 changed files with 434 additions and 83 deletions.
7 changes: 7 additions & 0 deletions src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -685,3 +685,10 @@ div.gutter {
width: 0.125em;
background: #ccc;
}

.project-settings-dialog {
td.MuiTableCell-root.MuiTableCell-body {
// Reduce the default 16px padding
padding: 8px;
}
}
90 changes: 79 additions & 11 deletions src/bindings/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { v4 as uuidV4 } from "uuid"
import * as IR from "../bindings/ir"
import { BundledMain } from "../bundled_files/Main.java"
import { BundledGitignore } from "../bundled_files/.gitignore"
import { BundledPreferences } from "../bundled_files/wpilib_preferences.json"
import { generateBundledPreferences } from "../bundled_files/wpilib_preferences.json"
import { generateRobotClass } from "../codegen/java/RobotGenerator"
import { BundledGradleBuild } from "../bundled_files/build.gradle"
import { generateReadme } from "../bundled_files/README.md"
import { BundledLaunchJson, BundledSettingsJson } from "../bundled_files/vscode"
import { BundledWpilibCommandsV2 } from "../bundled_files/vendordeps"
import { className } from "../codegen/java/util"
import { generateSubsystem } from "../codegen/java/SubsystemGenerator"

export type GeneratedFile = {
name: string
Expand All @@ -19,13 +21,57 @@ export type GeneratedFile = {
}

export type Project = {
name: string
controllers: Controller[]
subsystems: Subsystem[]
commands: IR.Group[]
generatedFiles: GeneratedFile[]
settings: Settings
};

export type SettingsKey = string
export type SettingsType = string | number | boolean | null
export type SettingsTypeName = "string" | "number" | "boolean"

export type Settings = Record<SettingsKey, SettingsType>

export type SettingsCategory = {
key: string
name: string
settings: SettingConfig[]
}

export type SettingConfig = {
/**
* A unique key to identify this setting. For example, a UUID or a unique identifier like "wpilib.epilogue.enabled".
*/
key: SettingsKey

/**
* The name of the setting to display to users.
*/
name: string

/**
* A description of this setting and what it does or how it's used.
*/
description: string

/**
* Whether or not the setting is required.
*/
required: boolean

/**
* The data type of the setting object. This will be used to determine the UI element that edits this setting.
*/
type: SettingsTypeName

/**
* The default value of the setting.
*/
defaultValue: SettingsType
}

const makeDefaultGeneratedFiles = (): GeneratedFile[] => {
return [
{
Expand All @@ -41,7 +87,7 @@ const makeDefaultGeneratedFiles = (): GeneratedFile[] => {
{
name: ".wpilib/wpilib_preferences.json",
description: "",
contents: BundledPreferences,
contents: "",
},
{
name: ".gitignore",
Expand Down Expand Up @@ -83,20 +129,25 @@ const makeDefaultGeneratedFiles = (): GeneratedFile[] => {

export const makeNewProject = (): Project => {
const project: Project = {
name: "New Project",
controllers: [
{ name: "New Controller", uuid: uuidV4(), type: "ps5", className: "CommandPS5Controller", fqn: "", port: 1 , buttons: [] },
],
subsystems: [],
commands: [],
generatedFiles: makeDefaultGeneratedFiles(),
}

// Update the robot class contents
project.generatedFiles.find(f => f.name === "src/main/java/frc/robot/Robot.java").contents = generateRobotClass(project)

// Update the readme
project.generatedFiles.find(f => f.name === "README.md").contents = generateReadme(project)
settings: {
"robotbuilder.general.project_name": "",
"robotbuilder.general.team_number": null,
// "robotbuilder.general.cache_sensor_values": false,
"wpilib.epilogue.enabled": true,
},
}

// Update pregenerated files to give them a valid initial state
// These files may need to be updated over time while the project is edited
updateFile(project, ".wpilib/wpilib_preferences.json", generateBundledPreferences(project))
updateFile(project, "src/main/java/frc/robot/Robot.java", generateRobotClass(project))
updateFile(project, "README.md", generateReadme(project))

return project
}
Expand All @@ -105,6 +156,23 @@ export function updateFile(project: Project, path: string, contents: string): vo
project.generatedFiles.find(f => f.name === path).contents = contents
}

/**
* Regenerates all dynamic project files.
*
* @param project the project to regenerate
*/
export function regenerateFiles(project: Project): void {
// Regenerate subsystems
project.subsystems.forEach(subsytem => {
updateFile(project, `src/main/java/frc/robot/subsystems/${ className(subsytem.name) }.java`, generateSubsystem(subsytem, project))
})

updateFile(project, "src/main/java/frc/robot/Robot.java", generateRobotClass(project))

updateFile(project, `README.md`, generateReadme(project))
updateFile(project, ".wpilib/wpilib_preferences.json", generateBundledPreferences(project))
}

export function findCommand(project: Project, commandOrId: AtomicCommand | IR.Group | string): AtomicCommand | IR.Group | null {
if (commandOrId instanceof AtomicCommand || commandOrId instanceof IR.Group) {
// Passed in a command object, return it
Expand Down
2 changes: 1 addition & 1 deletion src/bundled_files/README.md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { unindent } from "../codegen/java/util"

export const generateReadme = (project: Project): string => {
return unindent(`
# ${ project.name }
# ${ project.settings["robotbuilder.general.project_name"] }
This is your robot program!
`).trim()
Expand Down
19 changes: 11 additions & 8 deletions src/bundled_files/wpilib_preferences.json.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Project } from "../bindings/Project"
import { unindent } from "../codegen/java/util"

export const BundledPreferences = unindent(`
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2025",
"teamNumber": 9999
}
`).trim()
export const generateBundledPreferences = (project: Project): string => {
return unindent(`
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2025",
"teamNumber": ${ project.settings["robotbuilder.general.team_number"] }
}
`).trim()
}
52 changes: 30 additions & 22 deletions src/codegen/java/RobotGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ export function generateRobotClass(project: Project): string {
`
package frc.robot;
import edu.wpi.first.epilogue.Epilogue;
import edu.wpi.first.epilogue.Logged;
import edu.wpi.first.epilogue.NotLogged;
${ project.settings["wpilib.epilogue.enabled"] ? "import edu.wpi.first.epilogue.Epilogue;" : "" }
${ project.settings["wpilib.epilogue.enabled"] ? "import edu.wpi.first.epilogue.Logged;" : "" }
${ project.settings["wpilib.epilogue.enabled"] ? "import edu.wpi.first.epilogue.NotLogged;" : "" }
import edu.wpi.first.wpilibj.RuntimeType;
import edu.wpi.first.wpilibj.TimedRobot;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.CommandScheduler;
import frc.robot.subsystems.*;
@Logged
${ project.settings["wpilib.epilogue.enabled"] ? "@Logged" : "" }
public class Robot extends TimedRobot {
${
project.subsystems.map(s => generateSubsystemDeclaration(s)).join("\n")
project.subsystems.map(s => generateSubsystemDeclaration(project, s)).join("\n")
}
${
project.controllers.map(c => `
@NotLogged // Controllers are not loggable
${ project.settings["wpilib.epilogue.enabled"] ? `@NotLogged // Controllers are not loggable` : "" }
private final ${ c.className } ${ methodName(c.name) } = new ${ c.className }(${ c.port });
`)
}
Expand All @@ -36,27 +36,35 @@ ${
configureButtonBindings();
configureAutomaticBindings();
Epilogue.configure(config -> {
// TODO: Add a UI for customizing epilogue
if (getRuntimeType() == RuntimeType.kRoboRIO) {
// Only log to networktables on a roboRIO 1 because of limited disk space.
// If the disk fills up, there's a real risk of getting locked out of the rio!
config.dataLogger = new NTDataLogger(NetworkTablesInstance.getDefault());
} else {
// On a roboRIO 2 there's enough disk space to be able to safely log to disk
config.dataLogger = new FileSystemLogger(DataLogManager.getDataLog());
}
});
${ project.settings["wpilib.epilogue.enabled"] ? `
Epilogue.configure(config -> {
// TODO: Add a UI for customizing epilogue
if (getRuntimeType() == RuntimeType.kRoboRIO) {
// Only log to networktables on a roboRIO 1 because of limited disk space.
// If the disk fills up, there's a real risk of getting locked out of the rio!
config.dataLogger = new NTDataLogger(NetworkTablesInstance.getDefault());
} else {
// On a roboRIO 2 there's enough disk space to be able to safely log to disk
config.dataLogger = new FileSystemLogger(DataLogManager.getDataLog());
}
});
` : ""
}
}
@Override
public void robotPeriodic() {
// Run our commands
CommandScheduler.getInstance().run();
// Update our data logs
Epilogue.update(this);
${
project.settings["wpilib.epilogue.enabled"] ?
`
// Update our data logs
Epilogue.update(this);
` : ""
}
}
@Override
Expand Down Expand Up @@ -97,10 +105,10 @@ ${
)
}

function generateSubsystemDeclaration(subsystem: Subsystem): string {
function generateSubsystemDeclaration(project: Project, subsystem: Subsystem): string {
return indent(
`
@Logged(name = "${ subsystem.name }")
${ project.settings["wpilib.epilogue.enabled"] ? `@Logged(name = "${ subsystem.name }")` : "" }
private final ${ className(subsystem.name) } ${ methodName(subsystem.name) } = new ${ className(subsystem.name) }();
`.trim(),
2,
Expand Down
5 changes: 3 additions & 2 deletions src/codegen/java/StateGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Subsystem, SubsystemState } from "../../bindings/Command"
import { indent, methodName, unindent } from "./util"
import { generateStepInvocations, generateStepParams } from "./ActionGenerator"
import { Project } from "../../bindings/Project"

export function generateState(state: SubsystemState, subsystem: Subsystem): string {
export function generateState(project: Project, state: SubsystemState, subsystem: Subsystem): string {
console.log("[STATE-GENERATOR] Generating code for state", state)
return unindent(
`
@Logged(name = "${ state.name }?")
${ project.settings["wpilib.epilogue.enabled"] ? `@Logged(name = "${ state.name }?")` : "" }
public boolean ${ methodName(state.name) }(${ generateStepParams([state.step].filter(s => !!s), subsystem) }) {
${ generateStepInvocations([state.step].filter(s => !!s), subsystem).map(i => indent(`return ${ i }`, 6)).join("\n") }
}
Expand Down
12 changes: 7 additions & 5 deletions src/codegen/java/SubsystemGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ export function generateSubsystem(subsystem: Subsystem, project: Project) {
import static edu.wpi.first.units.Units.*;
${ [...new Set(subsystem.components.map(c => c.definition.fqn))].sort().map(fqn => indent(`import ${ fqn };`, 4)).join("\n") }
import edu.wpi.first.epilogue.Logged;
${ project.settings["wpilib.epilogue.enabled"] ? "import edu.wpi.first.epilogue.Logged;" : "" }
${ project.settings["wpilib.epilogue.enabled"] ? "import edu.wpi.first.epilogue.NotLogged;" : "" }
import edu.wpi.first.units.*;
import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard;
import edu.wpi.first.wpilibj.shuffleboard.ShuffleboardTab;
Expand All @@ -125,10 +127,10 @@ ${ [...new Set(subsystem.components.map(c => c.definition.fqn))].sort().map(fqn
/**
* The ${ subsystem.name } subsystem.
*/
@Logged
${ project.settings["wpilib.epilogue.enabled"] ? "@Logged" : "" }
public class ${ clazz } extends SubsystemBase {
${ subsystem.components.map(c => indent(`${ fieldDeclaration(c.definition.className, c.name) };`, 6)).join("\n") }
${ subsystem.components.map(c => indent(`${ fieldDeclaration(project, c.definition.className, c.name) };`, 6)).join("\n") }
${
(() => {
Expand All @@ -150,7 +152,7 @@ ${
${ subsystem.states.map(state => {
return indent(unindent(
`
@NotLogged
${ project.settings["wpilib.epilogue.enabled"] ? "@NotLogged" : "" }
public final Trigger ${ methodName(state.name) } = new Trigger(this::${ methodName(state.name) });
`,
).trim(), 6)
Expand Down Expand Up @@ -183,7 +185,7 @@ ${
// STATES
${
subsystem.states.map(state => unindent(indent(generateState(state, subsystem), 4)).trim())
subsystem.states.map(state => unindent(indent(generateState(project, state, subsystem), 4)).trim())
.map(f => indent(f, 6)).join("\n\n")
}
Expand Down
5 changes: 3 additions & 2 deletions src/codegen/java/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import parsers from "prettier-plugin-java"
import prettier from "prettier"
import { Subsystem } from "../../bindings/Command"
import { Project } from "../../bindings/Project"

export const MAIN_CLASS_PATH = "src/main/java/frc/robot/Main.java"
export const ROBOT_CLASS_PATH = "src/main/java/frc/robot/Robot.java"
Expand Down Expand Up @@ -92,9 +93,9 @@ export function objectName(name: string): string {
}
}

export function fieldDeclaration(type: string, name: string): string {
export function fieldDeclaration(project: Project, type: string, name: string): string {
return unindent(`
@Logged(name = "${ name }")
${ project.settings["wpilib.epilogue.enabled"] ? `@Logged(name = "${ name }")` : "" }
private final ${ type } ${ objectName(name) }
`).trim()
}
Expand Down
Loading

0 comments on commit e5f1199

Please sign in to comment.