Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
151 changes: 151 additions & 0 deletions external_samples/spark_mini.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""This is the spark_mini module.

This component wraps the RobotPy SparkMini, providing support for the REV
Robotics SPARKMini Motor Controller with PWM control.
"""

__author__ = "lizlooney@google.com (Liz Looney)"

from component import Component, PortType, InvalidPortException
import wpilib
import wpimath
import wpiutil

class SparkMiniComponent(Component):
def __init__(self, ports : list[tuple[PortType, int]]):
portType, port = ports[0]
if portType != PortType.SMART_MOTOR_PORT:
raise InvalidPortException
self.spark_mini = wpilib.SparkMini(port)

# Component methods

def get_manufacturer(self) -> str:
return "REV Robotics"

def get_name(self) -> str:
return "SPARKMini Motor Controller"

def get_part_number(self) -> str:
return "REV-31-1230"

def get_url(self) -> str:
return "https://www.revrobotics.com/rev-31-1230"

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

def stop(self) -> None:
# send stop command to motor
pass

def reset(self) -> None:
pass

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

def periodic(self) -> None:
pass

# Component specific methods

# Methods from wpilib.PWMMotorController

def add_follower(self, follower: wpilib.PWMMotorController) -> None:
'''Make the given PWM motor controller follow the output of this one.\n\n:param follower: The motor controller follower.'''
self.spark_mini.addFollower(follower)

def disable(self) -> None:
self.spark_mini.disable()

def enable_deadband_elimination(self, eliminateDeadband: bool) -> None:
'''Optionally eliminate the deadband from a motor controller.\n\n:param eliminateDeadband: If true, set the motor curve on the motor\n controller to eliminate the deadband in the middle\n of the range. Otherwise, keep the full range\n without modifying any values.'''
self.spark_mini.enableDeadbandElimination(eliminateDeadband)

def get(self) -> float:
'''Get the recently set value of the PWM. This value is affected by the\ninversion property. If you want the value that is sent directly to the\nMotorController, use PWM::GetSpeed() instead.\n\n:returns: The most recently set value for the PWM between -1.0 and 1.0.'''
return self.spark_mini.get()

def get_channel(self) -> int:
return self.spark_mini.getChannel()

def get_description(self) -> str:
return self.spark_mini.getDescription()

def get_inverted(self) -> bool:
return self.spark_mini.getInverted()

def get_voltage(self) -> wpimath.units.volts:
'''Gets the voltage output of the motor controller, nominally between -12 V\nand 12 V.\n\n:returns: The voltage of the motor controller, nominally between -12 V and 12\n V.'''
return self.spark_mini.getVoltage()

def set(self, value: float) -> None:
'''Set the PWM value.\n\nThe PWM value is set using a range of -1.0 to 1.0, appropriately scaling\nthe value for the FPGA.\n\n:param value: The speed value between -1.0 and 1.0 to set.'''
self.spark_mini.set(value)

def set_inverted(self, isInverted: bool) -> None:
self.spark_mini.setInverted(isInverted)

def set_voltage(self, output: wpimath.units.volts) -> None:
'''Sets the voltage output of the PWMMotorController. Compensates for\nthe current bus voltage to ensure that the desired voltage is output even\nif the battery voltage is below 12V - highly useful when the voltage\noutputs are "meaningful" (e.g. they come from a feedforward calculation).\n\nNOTE: This function *must* be called regularly in order for voltage\ncompensation to work properly - unlike the ordinary set function, it is not\n"set it and forget it."\n\n:param output: The voltage to output.'''
self.spark_mini.setVoltage(output)

def stop_motor(self) -> None:
self.spark_mini.stopMotor()

# Methods from wpilib.MotorSafety

def check(self) -> None:
'''Check if this motor has exceeded its timeout.\n\nThis method is called periodically to determine if this motor has exceeded\nits timeout value. If it has, the stop method is called, and the motor is\nshut down until its value is updated again.'''
self.spark_mini.check(follower)

# TODO(lizlooney): Decide whether we should expose checkMotors. It seems
# like it isn't intended to be called by users.
def check_motors() -> None:
'''Check the motors to see if any have timed out.\n\nThis static method is called periodically to poll all the motors and stop\nany that have timed out.'''
wpilib.SparkMini.checkMotors()

def feed(self) -> None:
'''Feed the motor safety object.\n\nResets the timer on this object that is used to do the timeouts.'''
self.spark_mini.feed()

def get_expiration(self) -> wpimath.units.seconds:
'''Retrieve the timeout value for the corresponding motor safety object.\n\n:returns: the timeout value.'''
return self.spark_mini.getExpiration()

def is_alive(self) -> bool:
'''Determine if the motor is still operating or has timed out.\n\n:returns: true if the motor is still operating normally and hasn't timed out.'''
return self.spark_mini.isAlive()

def is_safety_enabled(self) -> bool:
'''Return the state of the motor safety enabled flag.\n\nReturn if the motor safety is currently enabled for this device.\n\n:returns: True if motor safety is enforced for this device.'''
return self.spark_mini.isSafetyEnabled()

def set_expiration(self, expirationTime: wpimath.units.seconds) -> None:
'''Set the expiration time for the corresponding motor safety object.\n\n:param expirationTime: The timeout value.'''
self.spark_mini.setExpiration(expirationTime)

def set_safety_enabled(self, enabled: bool) -> None:
'''Enable/disable motor safety for this device.\n\nTurn on and off the motor safety option for this PWM object.\n\n:param enabled: True if motor safety is enforced for this object.'''
self.spark_mini.setSafetyEnabled(enabled)

# Methods from wpiutil.Sendable

def init_sendable(self, builder: wpiutil.SendableBuilder) -> None:
'''Initializes this Sendable object.\n\n:param builder: sendable builder'''
self.spark_mini.initSendable(builder)
2 changes: 1 addition & 1 deletion external_samples/sparkfun_led_stick.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def get_manufacturer(self) -> str:
return "SparkFun"

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

def get_part_number(self) -> str:
return "COM-18354"
Expand Down
84 changes: 80 additions & 4 deletions src/blocks/mrc_call_python_function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import * as Blockly from 'blockly';
import { Order } from 'blockly/python';

import { ClassMethodDefExtraState } from './mrc_class_method_def'
import { getAllowedTypesForSetCheck, getOutputCheck } from './utils/python';
import { FunctionData } from './utils/python_json_types';
import { getClassData, getAllowedTypesForSetCheck, getOutputCheck } from './utils/python';
import { FunctionData, findSuperFunctionData } from './utils/python_json_types';
import * as value from './utils/value';
import * as variable from './utils/variable';
import { Editor } from '../editor/editor';
Expand All @@ -34,6 +34,7 @@ import { createFieldDropdown } from '../fields/FieldDropdown';
import { createFieldNonEditableText } from '../fields/FieldNonEditableText';
import { MRC_STYLE_FUNCTIONS } from '../themes/styles'
import * as toolboxItems from '../toolbox/items';
import * as commonStorage from '../storage/common_storage';


// A block to call a python function.
Expand Down Expand Up @@ -217,6 +218,68 @@ function createInstanceMethodBlock(
return block;
}

export function addInstanceComponentBlocks(
componentType: string,
componentName: string,
contents: toolboxItems.ContentsType[]) {

const classData = getClassData(componentType);
const functions = classData.instanceMethods;

const componentClassData = getClassData('component.Component');
const componentFunctions = componentClassData.instanceMethods;

for (const functionData of functions) {
// Skip the functions that are also defined in componentFunctions.
if (findSuperFunctionData(functionData, componentFunctions)) {
continue;
}
const block = createInstanceComponentBlock(componentName, functionData);
contents.push(block);
}
}

function createInstanceComponentBlock(
componentName: string, functionData: FunctionData): toolboxItems.Block {
const extraState: CallPythonFunctionExtraState = {
functionKind: FunctionKind.INSTANCE_COMPONENT,
returnType: functionData.returnType,
args: [],
tooltip: functionData.tooltip,
importModule: '',
componentClassName: functionData.declaringClassName,
componentName: componentName,
};
const fields: {[key: string]: any} = {};
fields[FIELD_COMPONENT_NAME] = componentName;
fields[FIELD_FUNCTION_NAME] = functionData.functionName;
const inputs: {[key: string]: any} = {};
// For INSTANCE_COMPONENT functions, the 0 argument is 'self', but
// self is represented by the FIELD_COMPONENT_NAME field.
// We don't include the arg or input for self.
for (let i = 1; i < functionData.args.length; i++) {
const argData = functionData.args[i];
let argName = argData.name;
extraState.args.push({
'name': argName,
'type': argData.type,
});
// Check if we should plug a variable getter block into the argument input socket.
const input = value.valueForFunctionArgInput(argData.type, argData.defaultValue);
if (input) {
// Because we skipped the self argument, use i - 1 when filling the inputs array.
inputs['ARG' + (i - 1)] = input;
}
}
let block = new toolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null);
if (functionData.returnType && functionData.returnType != 'None') {
const varName = variable.varNameForType(functionData.returnType);
if (varName) {
block = variable.createVariableSetterBlock(varName, block);
}
}
return block;
}

//..............................................................................

Expand Down Expand Up @@ -475,7 +538,9 @@ const CALL_PYTHON_FUNCTION = {
break;
}
case FunctionKind.INSTANCE_COMPONENT: {
const componentNames = Editor.getComponentNames(this.workspace, this.mrcComponentClassName);
// TODO: We need the list of component names for this.mrcComponentClassName so we can
// create a dropdown that has the appropriate component names.
const componentNames = [];
const componentName = this.getComponentName();
if (!componentNames.includes(componentName)) {
componentNames.push(componentName);
Expand Down Expand Up @@ -612,7 +677,18 @@ export const pythonFromBlock = function(
const functionName = callPythonFunctionBlock.mrcActualFunctionName
? callPythonFunctionBlock.mrcActualFunctionName
: block.getFieldValue(FIELD_FUNCTION_NAME);
code = 'self.' + componentName + '.' + functionName;
// Generate the correct code depending on the module type.
switch (generator.getModuleType()) {
case commonStorage.MODULE_TYPE_PROJECT:
case commonStorage.MODULE_TYPE_MECHANISM:
code = 'self.';
break;
case commonStorage.MODULE_TYPE_OPMODE:
default:
code = 'self.robot.';
break;
}
code += componentName + '.' + functionName;
break;
}
default:
Expand Down
2 changes: 1 addition & 1 deletion src/blocks/utils/external_samples_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* @author lizlooney@google.com (Liz Looney)
*/

import { PythonData } from './python_json_types';
import { PythonData, ClassData } from './python_json_types';
import generatedExternalSamplesData from './generated/external_samples_data.json';

export const externalSamplesData = generatedExternalSamplesData as PythonData;
Loading