Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3696cd5
Added sample implementation for SparkFun LED Stick.
lizlooney Mar 14, 2025
c6d495c
Add md file to show the blocks for the SparkFun LED Stick.
lizlooney Mar 15, 2025
20a4078
Added toolbox for SparkFun LED Stick.
lizlooney Mar 15, 2025
8338151
Fixed typos.
lizlooney Mar 15, 2025
c8d7b7d
Specify the type of the color arguments.
lizlooney Mar 15, 2025
0138dc9
Merge branch 'main' of github.com:wpilibsuite/systemcore-blocks-inter…
lizlooney Mar 15, 2025
d3ec222
Merge branch 'main' of github.com:wpilibsuite/systemcore-blocks-inter…
lizlooney Mar 19, 2025
de754dd
Added support for calling component methods to the mrc_call_python_fu…
lizlooney Mar 21, 2025
0d1464e
Show the component name in the instance method call tooltip.
lizlooney Mar 24, 2025
7a0d635
Pass robot to OpMode.__init__.
lizlooney Mar 24, 2025
9c8e04e
Removed wpilib from the component names shown in the tooltips.
lizlooney Mar 25, 2025
0f3a9bb
Where component methods are called, generate self.robot.<component na…
lizlooney Mar 25, 2025
00e779a
Put toolbox categories in alphabetical order.
lizlooney Mar 25, 2025
c5d1374
Updated md file.
lizlooney Mar 25, 2025
56a64aa
more edits
lizlooney Mar 25, 2025
fd79e2a
Changed tooltip to use the word method instead of function.
lizlooney Mar 25, 2025
784159c
Specify widths of images.
lizlooney Mar 25, 2025
37125b9
Set image widths to 50%.
lizlooney Mar 25, 2025
87b928d
More f'ing around with image sizes.
lizlooney Mar 25, 2025
f119f8c
Renamed file to index.md.
lizlooney Mar 25, 2025
6705dfb
Merge branch 'main' of github.com:wpilibsuite/systemcore-blocks-inter…
lizlooney Mar 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added external_samples/docs/BlockWithTooltip.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added external_samples/docs/REV_ColorRangeSensor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added external_samples/docs/REV_Servo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added external_samples/docs/REV_SmartMotor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added external_samples/docs/REV_TouchSensor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added external_samples/docs/SparkFun_LEDStick.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions external_samples/docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Component Blocks

## Block Details

### Dropdown shows components of the specific type.

<img src="BlockWithComponentDropdown.png" width="291">

### Tooltip shows that the block calls a method on a component, followed by details about what that method does.

<img src="BlockWithTooltip.png" width="342">

## Component Toolbox Categories

### REV ColorRangeSensor

<img src="REV_ColorRangeSensor.png" width="531">

### REV Servo

<img src="REV_Servo.png" width="581">

### REV SmartMotor

<img src="REV_SmartMotor.png" width="622">

### REV TouchSensor

<img src="REV_TouchSensor.png" width="492">

### SparkFun LEDStick

<img src="SparkFun_LEDStick.png" width="666">
65 changes: 65 additions & 0 deletions external_samples/sparkfun_led_stick.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from component import Component, PortType, InvalidPortException
from enum import Enum
import wpilib

class SparkFunLEDStick(Component):
def __init__(self, ports : list[tuple[PortType, int]]):
portType, port = ports[0]
if portType != PortType.I2C_PORT:
raise InvalidPortException
self.port = port

# Component methods

def get_manufacturer(self) -> str:
return "SparkFun"

def get_name(self) -> str:
return "SparkFun Qwiic LED Strip"

def get_part_number(self) -> str:
return "COM-18354"

def get_url(self) -> str:
return "https://www.sparkfun.com/sparkfun-qwiic-led-stick-apa102c.html"

def get_version(self) -> tuple[int, int, str]:
return (1, 0, "")

def stop(self) -> None:
self.turn_all_off()

def reset(self) -> None:
pass

def get_connection_port_type(self) -> list[PortType]:
return [PortType.I2C_PORT]

def periodic(self) -> None:
pass

# SparkFunLEDStick methods

def set_color(self, position: int, color: wpilib.Color) -> None:
'''Change the color of an individual LED.'''
pass # TODO: implement

def set_color(self, color: wpilib.Color) -> None:
'''Change the color of all LEDs to a single color.'''
pass # TODO: implement

def set_colors(self, colors: list[int]) -> None:
'''Change the color of all LEDs using a list.'''
pass # TODO: implement

def set_brightness(self, position: int, brightness: int) -> None:
'''Set the brightness of an individual LED.'''
pass # TODO: implement

def set_brightness(self, brightness: int) -> None:
'''Set the brightness of all LEDs.'''
pass # TODO: implement

def turn_all_off(self) -> None:
'''Turn all LEDs off.'''
pass # TODO: implement
62 changes: 62 additions & 0 deletions src/blocks/mrc_call_python_function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ import * as Blockly from 'blockly';
import { Order } from 'blockly/python';

import * as pythonUtils from './utils/generated/python';
import { createFieldDropdown } from '../fields/FieldDropdown';
import { createFieldNonEditableText } from '../fields/FieldNonEditableText';
import { getAllowedTypesForSetCheck, getOutputCheck } from './utils/python';
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
import { MRC_STYLE_FUNCTIONS } from '../themes/styles'
import { ClassMethodDefExtraState } from './mrc_class_method_def'
import { Editor } from '../editor/editor';

// A block to call a python function.

Expand All @@ -40,10 +42,13 @@ enum FunctionKind {
CONSTRUCTOR = 'constructor',
INSTANCE = 'instance',
INSTANCE_WITHIN = 'instance_within',
INSTANCE_COMPONENT = 'instance_component'
}

const RETURN_TYPE_NONE = 'None';

const FIELD_COMPONENT_NAME = 'COMPONENT_NAME';

export type FunctionArg = {
name: string,
type: string,
Expand All @@ -58,6 +63,8 @@ interface CallPythonFunctionMixin extends CallPythonFunctionMixinType {
mrcImportModule: string,
mrcActualFunctionName: string,
mrcExportedFunction: boolean,
mrcComponentClassName: string,
mrcComponentName: string, // Do not access directly. Call getComponentName.
renameMethod(this: CallPythonFunctionBlock, newName: string): void;
mutateMethod(this: CallPythonFunctionBlock, defBlockExtraState: ClassMethodDefExtraState): void;
}
Expand Down Expand Up @@ -99,6 +106,14 @@ type CallPythonFunctionExtraState = {
* user's Project).
*/
exportedFunction: boolean,
/**
* The component name. Specified only if the function kind is INSTANCE_COMPONENT.
*/
componentName?: string,
/**
* The component class name. Specified only if the function kind is INSTANCE_COMPONENT.
*/
componentClassName?: string,
};

const CALL_PYTHON_FUNCTION = {
Expand Down Expand Up @@ -138,6 +153,13 @@ const CALL_PYTHON_FUNCTION = {
tooltip = 'Calls the method ' + functionName + '.';
break;
}
case FunctionKind.INSTANCE_COMPONENT: {
const className = this.mrcComponentClassName;
const functionName = this.getFieldValue(pythonUtils.FIELD_FUNCTION_NAME);
tooltip = 'Calls the method ' + className + '.' + functionName +
' on the component named ' + this.getComponentName() + '.';
break;
}
default:
throw new Error('mrcFunctionKind has unexpected value: ' + this.mrcFunctionKind)
}
Expand Down Expand Up @@ -174,6 +196,12 @@ const CALL_PYTHON_FUNCTION = {
if (this.mrcActualFunctionName) {
extraState.actualFunctionName = this.mrcActualFunctionName;
}
if (this.mrcComponentClassName) {
extraState.componentClassName = this.mrcComponentClassName;
}
if (this.getField(FIELD_COMPONENT_NAME)) {
extraState.componentName = this.getComponentName();
}
return extraState;
},
/**
Expand All @@ -199,6 +227,10 @@ const CALL_PYTHON_FUNCTION = {
? extraState.actualFunctionName : '';
this.mrcExportedFunction = extraState.exportedFunction
? extraState.exportedFunction : false;
this.mrcComponentClassName = extraState.componentClassName
? extraState.componentClassName : '';
this.mrcComponentName = extraState.componentName
? extraState.componentName : '';
this.updateBlock_();
},
/**
Expand Down Expand Up @@ -260,6 +292,19 @@ const CALL_PYTHON_FUNCTION = {
}
break;
}
case FunctionKind.INSTANCE_COMPONENT: {
const componentNames = Editor.getComponentNames(this.workspace, this.mrcComponentClassName);
const componentName = this.getComponentName();
if (!componentNames.includes(componentName)) {
componentNames.push(componentName);
}
this.appendDummyInput('TITLE')
.appendField('call')
.appendField(createFieldDropdown(componentNames), FIELD_COMPONENT_NAME)
.appendField('.')
.appendField(createFieldNonEditableText(''), pythonUtils.FIELD_FUNCTION_NAME);
break;
}
default:
throw new Error('mrcFunctionKind has unexpected value: ' + this.mrcFunctionKind)
}
Expand Down Expand Up @@ -293,6 +338,15 @@ const CALL_PYTHON_FUNCTION = {
this.removeInput('ARG' + i);
}
},
getComponentName(this: CallPythonFunctionBlock): string {
// If the COMPONENT_NAME field has been created, get the field value, which the user may have changed.
// If the COMPONENT_NAME field has not been created, get the component name from this.mrcComponentName.
return (this.getField(FIELD_COMPONENT_NAME))
? this.getFieldValue(FIELD_COMPONENT_NAME) : this.mrcComponentName;
},
getComponentClassName(this: CallPythonFunctionBlock): string {
return this.mrcComponentClassName;
},
renameMethod: function(this: CallPythonFunctionBlock, newName: string): void {
this.setFieldValue(newName, pythonUtils.FIELD_FUNCTION_NAME);
},
Expand Down Expand Up @@ -365,6 +419,14 @@ export const pythonFromBlock = function(
code = 'self.' + functionName;
break;
}
case FunctionKind.INSTANCE_COMPONENT: {
const componentName = callPythonFunctionBlock.getComponentName();
const functionName = callPythonFunctionBlock.mrcActualFunctionName
? callPythonFunctionBlock.mrcActualFunctionName
: block.getFieldValue(pythonUtils.FIELD_FUNCTION_NAME);
code = 'self.robot.' + componentName + '.' + functionName;
break;
}
default:
throw new Error('mrcFunctionKind has unexpected value: ' + callPythonFunctionBlock.mrcFunctionKind)
}
Expand Down
3 changes: 2 additions & 1 deletion src/blocks/mrc_class_method_def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,8 @@ export const pythonFromBlock = function (
xfix2 = xfix1;
}
if(block.mrcPythonMethodName == '__init__'){
branch = generator.defineClassVariables() + branch;
branch = generator.INDENT + 'super().__init__(robot)\n' +
generator.defineClassVariables() + branch;
}
if (returnValue) {
returnValue = generator.INDENT + 'return ' + returnValue + '\n';
Expand Down
55 changes: 54 additions & 1 deletion src/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
};

export class Editor {
private static workspaceIdToEditor: {[key: string]: Editor} = {};

private blocklyWorkspace: Blockly.WorkspaceSvg;
private generatorContext: GeneratorContext;
private storage: commonStorage.Storage;
Expand All @@ -47,6 +49,7 @@ export class Editor {
private toolbox: Blockly.utils.toolbox.ToolboxDefinition = EMPTY_TOOLBOX;

constructor(blocklyWorkspace: Blockly.WorkspaceSvg, generatorContext: GeneratorContext, storage: commonStorage.Storage) {
Editor.workspaceIdToEditor[blocklyWorkspace.id] = this;
this.blocklyWorkspace = blocklyWorkspace;
this.generatorContext = generatorContext;
this.storage = storage;
Expand Down Expand Up @@ -219,8 +222,17 @@ export class Editor {
const exportedBlocks = JSON.stringify(this.generatorContext.getExportedBlocks());
const blocksContent = JSON.stringify(
Blockly.serialization.workspaces.save(this.blocklyWorkspace));
const componentsContent = JSON.stringify(this.getComponents());
return commonStorage.makeModuleContent(
this.currentModule, pythonCode, exportedBlocks, blocksContent);
this.currentModule, pythonCode, blocksContent, exportedBlocks, componentsContent);
}

private getComponents(): commonStorage.Component[] {
const components: commonStorage.Component[] = [];
if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_PROJECT) {
// TODO(lizlooney): Fill the components array.
}
return components;
}

public async saveBlocks() {
Expand All @@ -233,4 +245,45 @@ export class Editor {
}
}

private getComponentNamesImpl(componentClassName: string): string[] {
if (!this.projectContent) {
throw new Error('getComponentNames: this.projectContent is null.');
}
const components = commonStorage.extractComponents(this.projectContent);

// TODO(lizlooney): Remove this fake code after getComponents (above) has been implemented.
components.push({name: 'frontTouch', className: 'rev.TouchSensor'});
components.push({name: 'backTouch', className: 'rev.TouchSensor'});
components.push({name: 'leftMotor', className: 'rev.SmartMotor'});
components.push({name: 'rightMotor', className: 'rev.SmartMotor'});
components.push({name: 'clawServo', className: 'rev.Servo'});
components.push({name: 'colorSensor', className: 'rev.ColorRangeSensor'});
components.push({name: 'ledStick', className: 'sparkfun.LEDStick'});
// End of fake code

const componentNames: string[] = [];
components.forEach((component) => {
if (component.className === componentClassName) {
componentNames.push(component.name);
}
});
return componentNames;
}

public static getComponentNames(
workspace: Blockly.Workspace, componentClassName: string): string[] {

let editor: Editor | null = null;
if (workspace.id in Editor.workspaceIdToEditor) {
editor = Editor.workspaceIdToEditor[workspace.id];
} else {
// If the workspace id was not found, it might be because the workspace is associated with the
// toolbox flyout, not a real workspace. In that case, use the first editor.
const allEditors = Object.values(Editor.workspaceIdToEditor);
if (allEditors.length) {
editor = allEditors[0];
}
}
return editor ? editor.getComponentNamesImpl(componentClassName) : [];
}
}
Loading