From c691ea67d69f82c2eff2472d6dbde431b783e3ec Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 18 Jul 2025 20:23:13 -0700 Subject: [PATCH 1/4] In common_storage.ts, save the events declared in the module. In mrc_event_handler.ts: Refactored hardcoded fields names into constants FIELD_SENDER and FIELD_EVENT_NAME. Added otherBlockId to EventHandlerExtraState and mrcOtherBlockId to EventHandlerMixin. Added onLoad method that updates event handler blocks if the event definition has been changed. Added functions addRobotEventHandlerBlocks and createRobotEventHandlerBlock to create blocks for the toolbox. In editor.ts, added method getEventsFromRobot. In hardware_category.ts, added function getRobotEventsBlocks and call it in getHardwareCategory when the current module is an opmode. --- src/blocks/mrc_event_handler.ts | 127 +++++++++++++++++++++++++++---- src/editor/editor.ts | 28 ++++++- src/storage/common_storage.ts | 68 ++++++++++++++--- src/toolbox/hardware_category.ts | 26 +++++++ 4 files changed, 220 insertions(+), 29 deletions(-) diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index 14174e90..6962ba86 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -23,13 +23,19 @@ import * as Blockly from 'blockly'; import {Order} from 'blockly/python'; -import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; -import {createFieldFlydown} from '../fields/field_flydown'; -import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; -import {MRC_STYLE_EVENT_HANDLER} from '../themes/styles'; +import { Editor } from '../editor/editor'; +import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; +import { createFieldFlydown } from '../fields/field_flydown'; +import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; +import { MRC_STYLE_EVENT_HANDLER } from '../themes/styles'; +import * as toolboxItems from '../toolbox/items'; +import * as commonStorage from '../storage/common_storage'; export const BLOCK_NAME = 'mrc_event_handler'; +const FIELD_SENDER = 'SENDER'; +const FIELD_EVENT_NAME = 'EVENT_NAME'; + export enum SenderType { ROBOT = 'robot', MECHANISM = 'mechanism', @@ -41,12 +47,15 @@ export interface Parameter { type?: string; } +const WARNING_ID_EVENT_CHANGED = 'event changed'; + export type EventHandlerBlock = Blockly.Block & EventHandlerMixin & Blockly.BlockSvg; interface EventHandlerMixin extends EventHandlerMixinType { mrcPathOfSender: string; mrcTypeOfSender: SenderType; mrcParameters: Parameter[]; + mrcOtherBlockId: string, } type EventHandlerMixinType = typeof EVENT_HANDLER; @@ -57,6 +66,8 @@ export interface EventHandlerExtraState { typeOfSender: SenderType; /** The parameters of the event handler. */ params: Parameter[]; + /** The id of the mrc_event block that defines the event. */ + otherBlockId: string, } const EVENT_HANDLER = { @@ -66,8 +77,8 @@ const EVENT_HANDLER = { init(this: EventHandlerBlock): void { this.appendDummyInput('TITLE') .appendField(Blockly.Msg.WHEN) - .appendField(createFieldNonEditableText('sender'), 'SENDER') - .appendField(createFieldNonEditableText('eventName'), 'EVENT_NAME'); + .appendField(createFieldNonEditableText('sender'), FIELD_SENDER) + .appendField(createFieldNonEditableText('eventName'), FIELD_EVENT_NAME); this.appendDummyInput('PARAMS') .appendField(Blockly.Msg.WITH); this.setOutput(false); @@ -86,8 +97,9 @@ const EVENT_HANDLER = { pathOfSender: this.mrcPathOfSender, typeOfSender: this.mrcTypeOfSender, params: [], + otherBlockId: this.mrcOtherBlockId, }; - + this.mrcParameters.forEach((param) => { extraState.params.push({ name: param.name, @@ -102,9 +114,10 @@ const EVENT_HANDLER = { * Applies the given state to this block. */ loadExtraState(this: EventHandlerBlock, extraState: EventHandlerExtraState): void { - this.mrcParameters = []; this.mrcPathOfSender = extraState.pathOfSender; this.mrcTypeOfSender = extraState.typeOfSender; + this.mrcParameters = []; + this.mrcOtherBlockId = extraState.otherBlockId; extraState.params.forEach((param) => { this.mrcParameters.push({ @@ -145,6 +158,58 @@ const EVENT_HANDLER = { input.removeField(fieldName); }); }, + onLoad: function(this: EventHandlerBlock): void { + // onLoad is called for each EventHandlerBlock when the blocks are loaded in the blockly workspace. + const warnings: string[] = []; + + // If this block is an event handler for a robot event, check that the robot event + // still exists and hasn't been changed. + // If the robot event doesn't exist, put a visible warning on this block. + // If the robot event has changed, update the block if possible or put a + // visible warning on it. + if (this.mrcTypeOfSender === SenderType.ROBOT) { + let foundRobotEvent = false; + const editor = Editor.getEditorForBlocklyWorkspace(this.workspace); + if (editor) { + const robotEvents = editor.getEventsFromRobot(); + for (const robotEvent of robotEvents) { + if (robotEvent.blockId === this.mrcOtherBlockId) { + foundRobotEvent = true; + + // If the event name has changed, we can fix this block. + if (this.getFieldValue(FIELD_EVENT_NAME) !== robotEvent.name) { + this.setFieldValue(robotEvent.name, FIELD_EVENT_NAME); + } + + this.mrcParameters = []; + robotEvent.args.forEach(arg => { + this.mrcParameters.push({ + name: arg.name, + type: arg.type, + }); + }); + this.mrcUpdateParams(); + + // Since we found the robot event, we can break out of the loop. + break; + } + } + if (!foundRobotEvent) { + warnings.push('This block is an event handler for an event that no longer exists.'); + } + } + } + + if (warnings.length) { + // Add a warnings to the block. + const warningText = warnings.join('\n\n'); + this.setWarningText(warningText, WARNING_ID_EVENT_CHANGED); + this.bringToFront(); + } else { + // Clear the existing warning on the block. + this.setWarningText(null, WARNING_ID_EVENT_CHANGED); + } + }, }; export function setup(): void { @@ -155,7 +220,10 @@ export function pythonFromBlock( block: EventHandlerBlock, generator: ExtendedPythonGenerator, ): string { - const blocklyName = `${block.getFieldValue('SENDER')}_${block.getFieldValue('EVENT_NAME')}`; + const sender = block.getFieldValue(FIELD_SENDER); + const eventName = block.getFieldValue(FIELD_EVENT_NAME); + + const blocklyName = `${sender}_${eventName}`; const funcName = generator.getProcedureName(blocklyName); let xfix1 = ''; @@ -211,12 +279,41 @@ export function pythonFromBlock( let code = `def ${funcName}(${paramString}):\n`; code += xfix1 + loopTrap + branch + xfix2 + returnValue; code = generator.scrub_(block, code); - + generator.addClassMethodDefinition(funcName, code); - generator.addEventHandler( - block.getFieldValue('SENDER'), - block.getFieldValue('EVENT_NAME'), - funcName); + generator.addEventHandler(sender, eventName, funcName); return ''; -} \ No newline at end of file +} + +// Functions used for creating blocks for the toolbox. + +export function addRobotEventHandlerBlocks( + events: commonStorage.Event[], + contents: toolboxItems.ContentsType[]) { + events.forEach(event => { + contents.push(createRobotEventHandlerBlock(event)); + }); +} + +function createRobotEventHandlerBlock( + event: commonStorage.Event): toolboxItems.Block { + const extraState: EventHandlerExtraState = { + // TODO(lizlooney): ask Alan what pathOfSender is for. + pathOfSender: '', + typeOfSender: SenderType.ROBOT, + params: [], + otherBlockId: event.blockId, + }; + event.args.forEach(arg => { + extraState.params.push({ + name: arg.name, + type: arg.type, + }); + }); + const fields: {[key: string]: any} = {}; + fields[FIELD_SENDER] = 'robot'; + fields[FIELD_EVENT_NAME] = event.name; + const inputs: {[key: string]: any} = {}; + return new toolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); +} diff --git a/src/editor/editor.ts b/src/editor/editor.ts index b790dab9..40a59f91 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -25,6 +25,7 @@ import { extendedPythonGenerator } from './extended_python_generator'; import { GeneratorContext } from './generator_context'; import * as commonStorage from '../storage/common_storage'; import * as callPythonFunction from '../blocks/mrc_call_python_function'; +import * as eventHandler from '../blocks/mrc_event_handler'; import * as classMethodDef from '../blocks/mrc_class_method_def'; import * as mechanismComponentHolder from '../blocks/mrc_mechanism_component_holder'; //import { testAllBlocksInToolbox } from '../toolbox/toolbox_tests'; @@ -80,8 +81,12 @@ export class Editor { if (blockCreateEvent.ids) { blockCreateEvent.ids.forEach(id => { const block = this.blocklyWorkspace.getBlockById(id); - if (block && block.type == callPythonFunction.BLOCK_NAME) { - (block as callPythonFunction.CallPythonFunctionBlock).onLoad(); + if (block) { + if (block.type == callPythonFunction.BLOCK_NAME) { + (block as callPythonFunction.CallPythonFunctionBlock).onLoad(); + } else if (block.type == eventHandler.BLOCK_NAME) { + (block as eventHandler.EventHandlerBlock).onLoad(); + } } }); } @@ -212,8 +217,10 @@ export class Editor { ? JSON.stringify(this.getMethodsForOutsideFromWorkspace()) : '[]'; const componentsContent = JSON.stringify(this.getComponentsFromWorkspace()); + const eventsContent = JSON.stringify(this.getEventsFromWorkspace()); return commonStorage.makeModuleContent( - this.currentModule, pythonCode, blocksContent, methodsContent, componentsContent); + this.currentModule, pythonCode, blocksContent, + methodsContent, componentsContent, eventsContent); } public getComponentsFromWorkspace(): commonStorage.Component[] { @@ -282,7 +289,7 @@ export class Editor { const events: commonStorage.Event[] = []; if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT || this.currentModule?.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { - // Get the holder block and ask it for the components. + // Get the holder block and ask it for the events. const holderBlocks = this.blocklyWorkspace.getBlocksByType(mechanismComponentHolder.BLOCK_NAME); holderBlocks.forEach(holderBlock => { const eventsFromHolder: commonStorage.Event[] = @@ -316,6 +323,19 @@ export class Editor { return commonStorage.extractComponents(this.robotContent); } + /** + * Returns the events defined in the robot. + */ + public getEventsFromRobot(): commonStorage.Event[] { + if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT) { + return this.getEventsFromWorkspace(); + } + if (!this.robotContent) { + throw new Error('getEventsFromRobot: this.robotContent is null.'); + } + return commonStorage.extractEvents(this.robotContent); + } + /** * Returns the methods defined in the robot. */ diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 1afe4a22..10cf8d74 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -90,11 +90,14 @@ const MARKER_BLOCKS_CONTENT = 'blocksContent: '; const MARKER_METHODS = 'methods: '; const MARKER_MODULE_TYPE = 'moduleType: '; const MARKER_COMPONENTS = 'components: '; +const MARKER_EVENTS = 'events: '; + const PARTS_INDEX_BLOCKS_CONTENT = 0; const PARTS_INDEX_METHODS = 1; const PARTS_INDEX_MODULE_TYPE = 2; const PARTS_INDEX_COMPONENTS = 3; -const NUMBER_OF_PARTS = 4; +const PARTS_INDEX_EVENTS = 4; +const NUMBER_OF_PARTS = 5; export const UPLOAD_DOWNLOAD_FILE_EXTENSION = '.blocks'; @@ -512,8 +515,14 @@ function startingBlocksToModuleContent( Blockly.serialization.workspaces.save(headlessBlocklyWorkspace)); const methodsContent = '[]'; const componentsContent = '[]'; + const eventsContent = '[]'; return makeModuleContent( - module, pythonCode, blocksContent, methodsContent, componentsContent); + module, + pythonCode, + blocksContent, + methodsContent, + componentsContent, + eventsContent); } /** @@ -568,16 +577,18 @@ export function newOpModeContent(projectName: string, opModeName: string): strin * Make the module content from the given python code and blocks content. */ export function makeModuleContent( - module: Module, - pythonCode: string, - blocksContent: string, - methodsContent: string, - componentsContent: string): string { + module: Module, + pythonCode: string, + blocksContent: string, + methodsContent: string, + componentsContent: string, + eventsContent: string): string { let delimiter = DELIMITER_PREFIX; while (module.moduleType.includes(delimiter) || blocksContent.includes(delimiter) || methodsContent.includes(delimiter) - || componentsContent.includes(delimiter)) { + || componentsContent.includes(delimiter) + || eventsContent.includes(delimiter)) { delimiter += '.'; } return ( @@ -585,6 +596,8 @@ export function makeModuleContent( pythonCode + '\n\n\n' + '"""\n' + delimiter + '\n' + + MARKER_EVENTS + eventsContent + '\n' + + delimiter + '\n' + MARKER_COMPONENTS + componentsContent + '\n' + delimiter + '\n' + MARKER_MODULE_TYPE + module.moduleType + '\n' + @@ -639,6 +652,10 @@ function getParts(moduleContent: string): string[] { // This module was saved without components. parts.push(MARKER_COMPONENTS + '[]'); } + if (parts.length <= PARTS_INDEX_EVENTS) { + // This module was saved without events. + parts.push(MARKER_EVENTS + '[]'); + } return parts; } @@ -692,6 +709,19 @@ export function extractComponents(moduleContent: string): Component[] { return components; } +/** + * Extract the events from the given module content. + */ +export function extractEvents(moduleContent: string): Event[] { + const parts = getParts(moduleContent); + let eventsContent = parts[PARTS_INDEX_EVENTS]; + if (eventsContent.startsWith(MARKER_EVENTS)) { + eventsContent = eventsContent.substring(MARKER_EVENTS.length); + } + const events: Event[] = JSON.parse(eventsContent); + return events; +} + /** * Produce the blob for downloading a project. */ @@ -737,6 +767,10 @@ function _processModuleContentForDownload( if (componentsContent.startsWith(MARKER_COMPONENTS)) { componentsContent = componentsContent.substring(MARKER_COMPONENTS.length); } + let eventsContent = parts[PARTS_INDEX_EVENTS]; + if (eventsContent.startsWith(MARKER_EVENTS)) { + eventsContent = eventsContent.substring(MARKER_EVENTS.length); + } const module: Module = { modulePath: makeModulePath(projectName, moduleName), @@ -750,7 +784,12 @@ function _processModuleContentForDownload( // Clear out the python content. const pythonCode = ''; return makeModuleContent( - module, pythonCode, blocksContent, methodsContent, componentsContent); + module, + pythonCode, + blocksContent, + methodsContent, + componentsContent, + eventsContent); } /** @@ -842,6 +881,10 @@ export function _processUploadedModule( if (componentsContent.startsWith(MARKER_COMPONENTS)) { componentsContent = componentsContent.substring(MARKER_COMPONENTS.length); } + let eventsContent = parts[PARTS_INDEX_EVENTS]; + if (eventsContent.startsWith(MARKER_EVENTS)) { + eventsContent = eventsContent.substring(MARKER_EVENTS.length); + } const moduleName = (moduleType === MODULE_TYPE_ROBOT) ? projectName : filename; @@ -869,6 +912,11 @@ export function _processUploadedModule( headlessBlocklyWorkspace, generatorContext); const moduleContent = makeModuleContent( - module, pythonCode, blocksContent, methodsContent, componentsContent); + module, + pythonCode, + blocksContent, + methodsContent, + componentsContent, + eventsContent); return [moduleName, moduleType, moduleContent]; } diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index 12a5258d..f87bb3fc 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -25,6 +25,7 @@ import * as toolboxItems from './items'; import { getAllPossibleMechanisms } from './blocks_mechanisms'; import { getAllPossibleComponents } from '../blocks/mrc_component'; import { getInstanceComponentBlocks, addInstanceRobotBlocks } from '../blocks/mrc_call_python_function'; +import { addRobotEventHandlerBlocks } from '../blocks/mrc_event_handler'; import { Editor } from '../editor/editor'; export function getHardwareCategory(currentModule: commonStorage.Module): toolboxItems.Category { @@ -49,6 +50,7 @@ export function getHardwareCategory(currentModule: commonStorage.Module): toolbo getRobotMechanismsBlocks(currentModule), getRobotComponentsBlocks(), getRobotMethodsBlocks(), + getRobotEventsBlocks(), ] }; } @@ -300,3 +302,27 @@ function getComponentsBlocks(hideParams : boolean): toolboxItems.Category { contents, }; } + +function getRobotEventsBlocks(): toolboxItems.Category { + // getRobotEventsBlocks is called when the user is editing an opmode. + // It allows the user to create event handlers for events previously defined in the Robot. + + const contents: toolboxItems.ContentsType[] = []; + + // Get the list of events from the robot and add the blocks for calling the + // robot functions. + const workspace = Blockly.getMainWorkspace(); + if (workspace) { + const editor = Editor.getEditorForBlocklyWorkspace(workspace); + if (editor) { + const eventsFromRobot = editor.getEventsFromRobot(); + addRobotEventHandlerBlocks(eventsFromRobot, contents); + } + } + + return { + kind: 'category', + name: Blockly.Msg['MRC_CATEGORY_EVENTS'], + contents, + }; +} From a6915bf337501aeda0692eee12ec9a71de13a6e6 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Sat, 19 Jul 2025 23:03:58 -0700 Subject: [PATCH 2/4] In extended_python_generator.ts: Added fields hasHardware and hasEventHandler. Set these in mrcWorkspaceToCode before calling super.workspaceToCode. Renamed method defineClassVariables to generateInitStatements. Renamed local variable variableDefinitions to initStatements. Removed setHasHardware and addHardwarePort. In mrc_components.ts, added methods getNewPort and getHardwarePorts. getHardwarePorts collects the hardware ports and is called by extended_python_generator before we start generating python for blocks. (Previous code called addHardwarePort when the mrc_component blocks was generating python, but that is too late.) In mrc_event_handler.ts, added function getHasEventHandler. This is used to determine whether the generated __init__ method needs to call register_events. In generator_context.ts, removed hasHardware, clear(), setHasHardware(), and getHasHardware(). In mrc_call_python_function.ts, use forEach loops where possible. Use BLOCK_NAME instead of 'mrc_call_python_function'. Moved code that iterates through mrc_mechanism_component_holder blocks from editor.ts to mrc_mechanism_component_holder.ts functions getComponents and getEvents. In mrc_mechanism_component_holder.ts added function getHardwarePorts to collect ports needed for define_hardware. Moved code that iterates through mrc_class_method_def blocks from editor.ts to mrc_class_method_def.ts functions getMethodsForWithin, getMethodsForOutside, and getMethodNamesAlreadyOverriddenInWorkspace. In robot_base.py, added register_event and unregister_event methods. In methods_category.ts, don't show blocks for robot methods register_event and unregister_event. --- .../blocks_base_classes/__init__.py | 2 +- .../blocks_base_classes/robot_base.py | 7 ++ src/blocks/mrc_call_python_function.ts | 10 +- src/blocks/mrc_class_method_def.ts | 43 ++++++++- src/blocks/mrc_component.ts | 26 +++-- src/blocks/mrc_event_handler.ts | 6 ++ src/blocks/mrc_mechanism.ts | 6 +- src/blocks/mrc_mechanism_component_holder.ts | 95 +++++++++++++++---- .../generated/server_python_scripts.json | 41 ++++++++ src/editor/editor.ts | 56 ++--------- src/editor/extended_python_generator.ts | 56 +++++------ src/editor/generator_context.ts | 16 ---- src/toolbox/methods_category.ts | 50 ++++++---- 13 files changed, 268 insertions(+), 146 deletions(-) diff --git a/server_python_scripts/blocks_base_classes/__init__.py b/server_python_scripts/blocks_base_classes/__init__.py index d59688f0..d4bc5ad3 100644 --- a/server_python_scripts/blocks_base_classes/__init__.py +++ b/server_python_scripts/blocks_base_classes/__init__.py @@ -5,4 +5,4 @@ from .robot_base import RobotBase __all__ = ['OpMode', 'Teleop', 'Auto', 'Test', 'Name', 'Group' - 'Mechanism', 'RobotBase'] \ No newline at end of file + 'Mechanism', 'RobotBase'] diff --git a/server_python_scripts/blocks_base_classes/robot_base.py b/server_python_scripts/blocks_base_classes/robot_base.py index c63129ce..1885caa7 100644 --- a/server_python_scripts/blocks_base_classes/robot_base.py +++ b/server_python_scripts/blocks_base_classes/robot_base.py @@ -1,8 +1,15 @@ # This is the class all robots derive from +from typing import Callable + class RobotBase: def __init__(self): self.hardware = [] + self.events = {} + def register_event(self, event_name: str, func: Callable) -> None: + self.events[event_name] = func + def unregister_event(self, event_name: str) -> None: + del self.events[event_name] def start(self) -> None: for hardware in self.hardware: hardware.start() diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index bf8073bf..b30ac83e 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -699,22 +699,22 @@ function generateCodeForArguments( } function getMethodCallers(workspace: Blockly.Workspace, otherBlockId: string): Blockly.Block[] { - return workspace.getBlocksByType('mrc_call_python_function').filter((block) => { + return workspace.getBlocksByType(BLOCK_NAME).filter((block) => { return (block as CallPythonFunctionBlock).mrcOtherBlockId === otherBlockId; }); } export function renameMethodCallers(workspace: Blockly.Workspace, otherBlockId: string, newName: string): void { - for (const block of getMethodCallers(workspace, otherBlockId)) { + getMethodCallers(workspace, otherBlockId).forEach(block => { (block as CallPythonFunctionBlock).renameMethodCaller(newName); - } + }); } export function mutateMethodCallers( workspace: Blockly.Workspace, otherBlockId: string, methodOrEvent: commonStorage.Method | commonStorage.Event) { const oldRecordUndo = Blockly.Events.getRecordUndo(); - for (const block of getMethodCallers(workspace, otherBlockId)) { + getMethodCallers(workspace, otherBlockId).forEach(block => { const callBlock = block as CallPythonFunctionBlock; // Get the extra state before changing the call block. const oldExtraState = callBlock.saveExtraState(); @@ -738,7 +738,7 @@ export function mutateMethodCallers( ); Blockly.Events.setRecordUndo(oldRecordUndo); } - } + }); } // Functions used for creating blocks for the toolbox. diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 979feb90..d6ea2138 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -36,7 +36,7 @@ import { MUTATOR_BLOCK_NAME, PARAM_CONTAINER_BLOCK_NAME, MethodMutatorArgBlock } export const BLOCK_NAME = 'mrc_class_method_def'; -const FIELD_METHOD_NAME = 'NAME'; +export const FIELD_METHOD_NAME = 'NAME'; type Parameter = { name: string, @@ -347,7 +347,7 @@ function findLegalMethodName(name: string, block: ClassMethodDefBlock): string { function isMethodNameUsed( name: string, workspace: Blockly.Workspace, opt_exclude?: Blockly.Block): boolean { const nameLowerCase = name.toLowerCase(); - for (const block of workspace.getBlocksByType('mrc_class_method_def')) { + for (const block of workspace.getBlocksByType(BLOCK_NAME)) { if (block === opt_exclude) { continue; } @@ -409,7 +409,7 @@ export const pythonFromBlock = function ( if (block.mrcPythonMethodName == '__init__') { let class_specific = generator.getClassSpecificForInit(); branch = generator.INDENT + 'super().__init__(' + class_specific + ')\n' + - generator.defineClassVariables() + branch; + generator.generateInitStatements() + branch; } else if (generator.inBaseClassMethod(blocklyName)){ // Special case for methods inherited from the based class: generate the @@ -510,3 +510,40 @@ function createClassMethodDefBlock( fields[FIELD_METHOD_NAME] = functionData.functionName; return new toolboxItems.Block(BLOCK_NAME, extraState, fields, null); } + +// Misc + +export function getMethodsForWithin( + workspace: Blockly.Workspace, + methods: commonStorage.Method[]): void { + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + const method = (block as classMethodDef.ClassMethodDefBlock).getMethodForWithin(); + if (method) { + methods.push(method); + } + }); +} + +export function getMethodsForOutside( + workspace: Blockly.Workspace, + methods: commonStorage.Method[]): void { + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + const method = (block as classMethodDef.ClassMethodDefBlock).getMethodForOutside(); + if (method) { + methods.push(method); + } + }); +} + +export function getMethodNamesAlreadyOverriddenInWorkspace( + workspace: Blockly.Workspace, + methodNamesAlreadyOverridden: string[]): void { + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + const methodDefBlock = block as classMethodDef.ClassMethodDefBlock; + // If the user cannot change the signature, it means the block defines a method that overrides a baseclass method. + // That's what we are looking for here. + if (!methodDefBlock.canChangeSignature()) { + methodNamesAlreadyOverridden.push(methodDefBlock.getMethodName()); + } + }); +} diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index 0c19c032..c5210086 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -146,6 +146,24 @@ const COMPONENT = { className: componentType, }; }, + getNewPort: function (this: ComponentBlock, i: number): string { + let extension = ''; + if (i != 0) { + extension = '_' + (i + 1).toString(); + } + return block.getFieldValue(FIELD_NAME) + extension + '_port'; + }, + getHardwarePorts: function (this: ComponentBlock, ports: {[key: string]: string}): void { + // Collect the hardware ports for this component block that are needed to generate + // the robot's define_hardware method. (The key is the port, the value is the type.) + if (this.hideParams) { + for (let i = 0; i < this.mrcArgs.length; i++) { + const fieldName = 'ARG' + i; + const newPort = this.getNewPort(i); + ports[newPort] = this.mrcArgs[i].type; + } + } + }, } export const setup = function () { @@ -171,13 +189,7 @@ export const pythonFromBlock = function ( code += ', ' } if (block.hideParams) { - let extension = ''; - if (i != 0) { - extension = '_' + (i + 1).toString(); - } - const newPort = block.getFieldValue(FIELD_NAME) + extension + '_port'; - generator.addHardwarePort(newPort, block.mrcArgs[i].type); - code += block.mrcArgs[i].name + ' = ' + newPort; + code += block.mrcArgs[i].name + ' = ' + block.getNewPort(i); } else { code += block.mrcArgs[i].name + ' = ' + generator.valueToCode(block, fieldName, Order.NONE); } diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index 6962ba86..b9c4aacc 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -317,3 +317,9 @@ function createRobotEventHandlerBlock( const inputs: {[key: string]: any} = {}; return new toolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); } + +// Misc + +export function getHasEventHandler(workspace: Blockly.Workspace): boolean { + return workspace.getBlocksByType(BLOCK_NAME).length > 0; +} diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index 340a0de6..57fe6889 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -111,7 +111,11 @@ const MECHANISM = { input.setCheck(getAllowedTypesForSetCheck(this.mrcArgs[i].type)); } } - } + }, + getHardwarePorts: function (this: MechanismBlock, ports: {[key: string]: string}): void { + // TODO: Collect the hardware ports for this mechanism block that are needed to generate + // the robot's define_hardware method. (The key is the port, the value is the type.) + }, } export const setup = function () { diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 310fd49c..592d5605 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -37,8 +37,9 @@ import { EventBlock } from './mrc_event'; export const BLOCK_NAME = 'mrc_mechanism_component_holder'; -export const MECHANISM = 'mechanism'; -export const COMPONENT = 'component'; +const INPUT_MECHANISMS = 'MECHANISMS'; +const INPUT_COMPONENTS = 'COMPONENTS'; +const INPUT_EVENTS = 'EVENTS'; export const TOOLBOX_UPDATE_EVENT = 'toolbox-update-requested'; @@ -73,9 +74,9 @@ const MECHANISM_COMPONENT_HOLDER = { */ init: function (this: MechanismComponentHolderBlock): void { this.setInputsInline(false); - this.appendStatementInput('MECHANISMS').setCheck(MECHANISM_OUTPUT).appendField(Blockly.Msg.MECHANISMS); - this.appendStatementInput('COMPONENTS').setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.COMPONENTS); - this.appendStatementInput('EVENTS').setCheck(EVENT_OUTPUT).appendField(Blockly.Msg.EVENTS); + this.appendStatementInput(INPUT_MECHANISMS).setCheck(MECHANISM_OUTPUT).appendField(Blockly.Msg.MECHANISMS); + this.appendStatementInput(INPUT_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.COMPONENTS); + this.appendStatementInput(INPUT_EVENTS).setCheck(EVENT_OUTPUT).appendField(Blockly.Msg.EVENTS); this.setOutput(false); @@ -104,14 +105,14 @@ const MECHANISM_COMPONENT_HOLDER = { */ updateBlock_: function (this: MechanismComponentHolderBlock): void { if (this.mrcHideMechanisms) { - if (this.getInput('MECHANISMS')) { - this.removeInput('MECHANISMS') + if (this.getInput(INPUT_MECHANISMS)) { + this.removeInput(INPUT_MECHANISMS) } } else { - if (this.getInput('MECHANISMS') == null) { - this.appendStatementInput('MECHANISMS').setCheck(MECHANISM_OUTPUT).appendField('Mechanisms'); - this.moveInputBefore('MECHANISMS', 'COMPONENTS') + if (this.getInput(INPUT_MECHANISMS) == null) { + this.appendStatementInput(INPUT_MECHANISMS).setCheck(MECHANISM_OUTPUT).appendField('Mechanisms'); + this.moveInputBefore(INPUT_MECHANISMS, INPUT_COMPONENTS) } } }, @@ -134,7 +135,7 @@ const MECHANISM_COMPONENT_HOLDER = { const components: commonStorage.Component[] = [] // Get component blocks from the COMPONENTS input - const componentsInput = this.getInput('COMPONENTS'); + const componentsInput = this.getInput(INPUT_COMPONENTS); if (componentsInput && componentsInput.connection) { // Walk through all connected component blocks. let componentBlock = componentsInput.connection.targetBlock(); @@ -156,7 +157,7 @@ const MECHANISM_COMPONENT_HOLDER = { const events: commonStorage.Event[] = [] // Get event blocks from the EVENTS input - const eventsInput = this.getInput('EVENTS'); + const eventsInput = this.getInput(INPUT_EVENTS); if (eventsInput && eventsInput.connection) { // Walk through all connected event blocks. let eventBlock = eventsInput.connection.targetBlock(); @@ -200,8 +201,8 @@ function pythonFromBlockInRobot(block: MechanismComponentHolderBlock, generator: let mechanisms = ''; let components = ''; - mechanisms = generator.statementToCode(block, 'MECHANISMS'); - components = generator.statementToCode(block, 'COMPONENTS'); + mechanisms = generator.statementToCode(block, INPUT_MECHANISMS); + components = generator.statementToCode(block, INPUT_COMPONENTS); const body = mechanisms + components; if (body != '') { @@ -213,7 +214,7 @@ function pythonFromBlockInRobot(block: MechanismComponentHolderBlock, generator: function pythonFromBlockInMechanism(block: MechanismComponentHolderBlock, generator: ExtendedPythonGenerator) { let components = ''; - components = generator.statementToCode(block, 'COMPONENTS'); + components = generator.statementToCode(block, INPUT_COMPONENTS); let code = 'def define_hardware(self' + generator.getListOfPorts(false) + '):\n'; @@ -227,7 +228,7 @@ export const pythonFromBlock = function ( block: MechanismComponentHolderBlock, generator: ExtendedPythonGenerator, ) { - if (block.getInput('MECHANISMS')) { + if (block.getInput(INPUT_MECHANISMS)) { pythonFromBlockInRobot(block, generator); } else { @@ -235,3 +236,65 @@ export const pythonFromBlock = function ( } return '' } + +// Misc + +/** + * Collects the ports for hardware (mechanisms and components). + * Returns true if the given workspace has a mrc_mechanism_component_holder + * block that contains at least one component or mechanism. + */ +export function getHardwarePorts(workspace: Blockly.Workspace, ports: {[key: string]: string}): boolean { + let hasHardware = false; + for (const block of workspace.getBlocksByType(BLOCK_NAME)) { + const mechanismsInput = block.getInput(INPUT_MECHANISMS); + if (mechanismsInput && mechanismsInput.connection) { + // Walk through all connected mechanism blocks. + let mechanismBlock = mechanismsInput.connection.targetBlock(); + while (mechanismBlock) { + if (mechanismBlock.type === MRC_MECHANISM_NAME) { + hasHardware = true; + (mechanismBlock as MechanismBlock).getHardwarePorts(ports); + } + // Move to the next block in the chain + mechanismBlock = mechanismBlock.getNextBlock(); + } + } + const componentsInput = block.getInput(INPUT_COMPONENTS); + if (componentsInput && componentsInput.connection) { + // Walk through all connected component blocks. + let componentBlock = componentsInput.connection.targetBlock(); + while (componentBlock) { + if (componentBlock.type === MRC_COMPONENT_NAME) { + hasHardware = true; + (componentBlock as ComponentBlock).getHardwarePorts(ports); + } + // Move to the next block in the chain + componentBlock = componentBlock.getNextBlock(); + } + } + } + return hasHardware; +} + +export function getComponents( + workspace: Blockly.Workspace, + components: commonStorage.Component[]): void { + // Get the holder block and ask it for the components. + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + const componentsFromHolder: commonStorage.Component[] = + (block as MechanismComponentHolderBlock).getComponents(); + components.push(...componentsFromHolder); + }); +} + +export function getEvents( + workspace: Blockly.Workspace, + events: commonStorage.Event[]): void { + // Get the holder block and ask it for the events. + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + const eventsFromHolder: commonStorage.Event[] = + (block as MechanismComponentHolderBlock).getEvents(); + events.push(...eventsFromHolder); + }); +} diff --git a/src/blocks/utils/generated/server_python_scripts.json b/src/blocks/utils/generated/server_python_scripts.json index 7ca10aba..2f4cec9c 100644 --- a/src/blocks/utils/generated/server_python_scripts.json +++ b/src/blocks/utils/generated/server_python_scripts.json @@ -137,6 +137,29 @@ ], "enums": [], "instanceMethods": [ + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.RobotBase" + }, + { + "defaultValue": "", + "name": "event_name", + "type": "str" + }, + { + "defaultValue": "", + "name": "func", + "type": "typing.Callable" + } + ], + "declaringClassName": "blocks_base_classes.RobotBase", + "functionName": "register_event", + "returnType": "None", + "tooltip": "" + }, { "args": [ { @@ -163,6 +186,24 @@ "returnType": "None", "tooltip": "" }, + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.RobotBase" + }, + { + "defaultValue": "", + "name": "event_name", + "type": "str" + } + ], + "declaringClassName": "blocks_base_classes.RobotBase", + "functionName": "unregister_event", + "returnType": "None", + "tooltip": "" + }, { "args": [ { diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 40a59f91..ae96ba73 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -226,76 +226,36 @@ export class Editor { public getComponentsFromWorkspace(): commonStorage.Component[] { const components: commonStorage.Component[] = []; if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT || - this.currentModule?.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { - // Get the holder block and ask it for the components. - const holderBlocks = this.blocklyWorkspace.getBlocksByType(mechanismComponentHolder.BLOCK_NAME); - holderBlocks.forEach(holderBlock => { - const componentsFromHolder: commonStorage.Component[] = - (holderBlock as mechanismComponentHolder.MechanismComponentHolderBlock).getComponents(); - components.push(...componentsFromHolder); - }); + this.currentModule?.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { + mechanismComponentHolder.getComponents(this.blocklyWorkspace, components); } return components; } public getMethodsForWithinFromWorkspace(): commonStorage.Method[] { const methods: commonStorage.Method[] = []; - - // Get the class method definition blocks. - const methodDefBlocks = this.blocklyWorkspace.getBlocksByType(classMethodDef.BLOCK_NAME); - methodDefBlocks.forEach(methodDefBlock => { - const method = (methodDefBlock as classMethodDef.ClassMethodDefBlock).getMethodForWithin(); - if (method) { - methods.push(method); - } - }); - + classMethodDef.getMethodsForWithin(this.blocklyWorkspace, methods); return methods; } public getMethodsForOutsideFromWorkspace(): commonStorage.Method[] { const methods: commonStorage.Method[] = []; - - // Get the class method definition blocks. - const methodDefBlocks = this.blocklyWorkspace.getBlocksByType(classMethodDef.BLOCK_NAME); - methodDefBlocks.forEach(methodDefBlock => { - const method = (methodDefBlock as classMethodDef.ClassMethodDefBlock).getMethodForOutside(); - if (method) { - methods.push(method); - } - }); - + classMethodDef.getMethodsForOutside(this.blocklyWorkspace, methods); return methods; } public getMethodNamesAlreadyOverriddenInWorkspace(): string[] { const methodNamesAlreadyOverridden: string[] = []; - - // Get the class method definition blocks. - const methodDefBlocks = this.blocklyWorkspace.getBlocksByType(classMethodDef.BLOCK_NAME); - methodDefBlocks.forEach(block => { - const methodDefBlock = block as classMethodDef.ClassMethodDefBlock; - // If the user cannot change the signature, it means the block defines a method that overrides a baseclass method. - // That's what we are looking for here. - if (!methodDefBlock.canChangeSignature()) { - methodNamesAlreadyOverridden.push(methodDefBlock.getMethodName()); - } - }); - + classMethodDef.getMethodNamesAlreadyOverriddenInWorkspace( + this.blocklyWorkspace, methodNamesAlreadyOverridden); return methodNamesAlreadyOverridden; } public getEventsFromWorkspace(): commonStorage.Event[] { const events: commonStorage.Event[] = []; if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT || - this.currentModule?.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { - // Get the holder block and ask it for the events. - const holderBlocks = this.blocklyWorkspace.getBlocksByType(mechanismComponentHolder.BLOCK_NAME); - holderBlocks.forEach(holderBlock => { - const eventsFromHolder: commonStorage.Event[] = - (holderBlock as mechanismComponentHolder.MechanismComponentHolderBlock).getEvents(); - events.push(...eventsFromHolder); - }); + this.currentModule?.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { + mechanismComponentHolder.getEvents(this.blocklyWorkspace, events); } return events; } diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index c7f4abdc..3b5db6ab 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -22,7 +22,8 @@ import * as Blockly from 'blockly/core'; import { PythonGenerator } from 'blockly/python'; import { GeneratorContext } from './generator_context'; -import * as MechanismContainerHolder from '../blocks/mrc_mechanism_component_holder'; +import * as mechanismContainerHolder from '../blocks/mrc_mechanism_component_holder'; +import * as eventHandler from '../blocks/mrc_event_handler'; import { MODULE_NAME_BLOCKS_BASE_CLASSES, CLASS_NAME_OPMODE, @@ -69,10 +70,16 @@ export class ExtendedPythonGenerator extends PythonGenerator { private workspace: Blockly.Workspace | null = null; private context: GeneratorContext | null = null; + // Has components or mechanisms (ie, needs to call self.define_hardware in __init__) + private hasHardware = false; + private ports: {[key: string]: string} = Object.create(null); + + // Has event handlers (ie, needs to call self.register_events in __init__) + private hasEventHandler = false; + private classMethods: {[key: string]: string} = Object.create(null); private events: {[key: string]: {sender: string, eventName: string}} = Object.create(null); - private ports: {[key: string]: string} = Object.create(null); - // Opmode details + // Opmode details private details : OpModeDetails | null = null; constructor() { @@ -100,38 +107,33 @@ export class ExtendedPythonGenerator extends PythonGenerator { * This is called from the python generator for the mrc_class_method_def for the * init method */ - defineClassVariables() : string { - let variableDefinitions = ''; - - if (this.context?.getHasHardware()) { - if ('define_hardware' in this.classMethods) { - variableDefinitions += this.INDENT + "self.define_hardware("; - variableDefinitions += this.getListOfPorts(true); - variableDefinitions += ')\n'; - } - if (this.events && Object.keys(this.events).length > 0){ - variableDefinitions += this.INDENT + "self.register_events()\n"; - } + generateInitStatements() : string { + let initStatements = ''; + + if (this.hasHardware) { + initStatements += this.INDENT + "self.define_hardware("; + initStatements += this.getListOfPorts(true); + initStatements += ')\n'; + } + if (this.hasEventHandler) { + initStatements += this.INDENT + "self.register_events()\n"; } - return variableDefinitions; + return initStatements; } + getVariableName(nameOrId: string): string { const varName = super.getVariableName(nameOrId); return "self." + varName; } - setHasHardware() : void{ - this.context?.setHasHardware(); - } mrcWorkspaceToCode(workspace: Blockly.Workspace, context: GeneratorContext): string { this.workspace = workspace; this.context = context; - this.context.clear(); - if (this.workspace.getBlocksByType(MechanismContainerHolder.BLOCK_NAME).length > 0){ - this.setHasHardware(); - } + this.ports = Object.create(null); + this.hasHardware = mechanismContainerHolder.getHardwarePorts(this.workspace, this.ports); + this.hasEventHandler = eventHandler.getHasEventHandler(this.workspace); const code = super.workspaceToCode(workspace); @@ -175,14 +177,8 @@ export class ExtendedPythonGenerator extends PythonGenerator { addEventHandler(sender: string, eventName: string, funcName: string): void { this.events[funcName] = { 'sender': sender, - 'eventName': eventName,} + 'eventName': eventName } - - /** - * Add a Hardware Port - */ - addHardwarePort(portName: string, type: string): void{ - this.ports[portName] = type; } getListOfPorts(startWithFirst: boolean): string{ diff --git a/src/editor/generator_context.ts b/src/editor/generator_context.ts index 32543a8c..ddd62cec 100644 --- a/src/editor/generator_context.ts +++ b/src/editor/generator_context.ts @@ -30,12 +30,8 @@ export function createGeneratorContext(): GeneratorContext { export class GeneratorContext { private module: commonStorage.Module | null = null; - // Has mechanisms (ie, needs in init) - private hasHardware = false; - setModule(module: commonStorage.Module | null) { this.module = module; - this.clear(); } getModuleType(): string | null { @@ -45,18 +41,6 @@ export class GeneratorContext { return null; } - clear(): void { - this.hasHardware= false; - } - - setHasHardware():void{ - this.hasHardware = true; - } - - getHasHardware():boolean{ - return this.hasHardware; - } - getClassName(): string { if (!this.module) { throw new Error('getClassName: this.module is null.'); diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index 8415c784..4f3b917b 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -26,7 +26,7 @@ import * as commonStorage from '../storage/common_storage'; import { MRC_CATEGORY_STYLE_METHODS } from '../themes/styles'; import { CLASS_NAME_ROBOT_BASE, CLASS_NAME_OPMODE, CLASS_NAME_MECHANISM } from '../blocks/utils/python'; import { addInstanceWithinBlocks } from '../blocks/mrc_call_python_function'; -import { createCustomMethodBlock, getBaseClassBlocks } from '../blocks/mrc_class_method_def'; +import { createCustomMethodBlock, getBaseClassBlocks, FIELD_METHOD_NAME } from '../blocks/mrc_class_method_def'; import { Editor } from '../editor/editor'; @@ -67,19 +67,26 @@ export class MethodsCategory { if (this.currentModule) { if (this.currentModule.moduleType == commonStorage.MODULE_TYPE_ROBOT) { + // TODO(lizlooney): We need a way to mark a method in python as not overridable. + // For example, in RobotBase, register_event and unregister_event should not be + // overridden in a user's robot. + const methodNamesNotOverrideable: string[] = [ + 'register_event', + 'unregister_event', + ]; // Add the methods for a Robot. this.addClassBlocksForCurrentModule( - 'More Robot Methods', this.robotClassBlocks, + 'More Robot Methods', this.robotClassBlocks, methodNamesNotOverrideable, methodNamesAlreadyOverridden, contents); } else if (this.currentModule.moduleType == commonStorage.MODULE_TYPE_MECHANISM) { // Add the methods for a Mechanism. this.addClassBlocksForCurrentModule( - 'More Mechanism Methods', this.mechanismClassBlocks, + 'More Mechanism Methods', this.mechanismClassBlocks, [], methodNamesAlreadyOverridden, contents); } else if (this.currentModule.moduleType == commonStorage.MODULE_TYPE_OPMODE) { // Add the methods for an OpMode. this.addClassBlocksForCurrentModule( - 'More OpMode Methods', this.opmodeClassBlocks, + 'More OpMode Methods', this.opmodeClassBlocks, [], methodNamesAlreadyOverridden, contents); } } @@ -108,26 +115,31 @@ export class MethodsCategory { } private addClassBlocksForCurrentModule( - label: string, class_blocks: toolboxItems.Block[], + label: string, classBlocks: toolboxItems.Block[], + methodNamesNotOverrideable: string[], methodNamesAlreadyOverridden: string[], contents: toolboxItems.ContentsType[]) { let labelAdded = false; - class_blocks.forEach((blockInfo) => { + for (const blockInfo of classBlocks) { if (blockInfo.fields) { - const methodName = blockInfo.fields['NAME']; - if (!methodNamesAlreadyOverridden.includes(methodName)) { - if (!labelAdded) { - contents.push( - { - kind: 'label', - text: label, - }, - ); - labelAdded = true; - } - contents.push(blockInfo); + const methodName = blockInfo.fields[FIELD_METHOD_NAME]; + if (methodNamesNotOverrideable.includes(methodName)) { + continue; } + if (methodNamesAlreadyOverridden.includes(methodName)) { + continue; + } + if (!labelAdded) { + contents.push( + { + kind: 'label', + text: label, + }, + ); + labelAdded = true; + } + contents.push(blockInfo); } - }); + } } } From 3e52abf7478a9fdc3ecd2ab909692360b5bc3dff Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Sat, 19 Jul 2025 23:58:44 -0700 Subject: [PATCH 3/4] Fixed typescript errors. --- src/blocks/mrc_class_method_def.ts | 6 +++--- src/blocks/mrc_component.ts | 7 +++---- src/blocks/mrc_event.ts | 4 ++-- src/blocks/mrc_mechanism.ts | 6 +++--- src/blocks/mrc_mechanism_component_holder.ts | 8 +++----- src/blocks/mrc_port.ts | 14 ++++++-------- 6 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index d6ea2138..167f62c7 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -517,7 +517,7 @@ export function getMethodsForWithin( workspace: Blockly.Workspace, methods: commonStorage.Method[]): void { workspace.getBlocksByType(BLOCK_NAME).forEach(block => { - const method = (block as classMethodDef.ClassMethodDefBlock).getMethodForWithin(); + const method = (block as ClassMethodDefBlock).getMethodForWithin(); if (method) { methods.push(method); } @@ -528,7 +528,7 @@ export function getMethodsForOutside( workspace: Blockly.Workspace, methods: commonStorage.Method[]): void { workspace.getBlocksByType(BLOCK_NAME).forEach(block => { - const method = (block as classMethodDef.ClassMethodDefBlock).getMethodForOutside(); + const method = (block as ClassMethodDefBlock).getMethodForOutside(); if (method) { methods.push(method); } @@ -539,7 +539,7 @@ export function getMethodNamesAlreadyOverriddenInWorkspace( workspace: Blockly.Workspace, methodNamesAlreadyOverridden: string[]): void { workspace.getBlocksByType(BLOCK_NAME).forEach(block => { - const methodDefBlock = block as classMethodDef.ClassMethodDefBlock; + const methodDefBlock = block as ClassMethodDefBlock; // If the user cannot change the signature, it means the block defines a method that overrides a baseclass method. // That's what we are looking for here. if (!methodDefBlock.canChangeSignature()) { diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index c5210086..717e8f81 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -39,7 +39,7 @@ export const OUTPUT_NAME = 'mrc_component'; export const FIELD_NAME = 'NAME'; export const FIELD_TYPE = 'TYPE'; -export type ConstructorArg = { +type ConstructorArg = { name: string, type: string, }; @@ -151,14 +151,13 @@ const COMPONENT = { if (i != 0) { extension = '_' + (i + 1).toString(); } - return block.getFieldValue(FIELD_NAME) + extension + '_port'; + return this.getFieldValue(FIELD_NAME) + extension + '_port'; }, getHardwarePorts: function (this: ComponentBlock, ports: {[key: string]: string}): void { // Collect the hardware ports for this component block that are needed to generate - // the robot's define_hardware method. (The key is the port, the value is the type.) + // the define_hardware method. (The key is the port, the value is the type.) if (this.hideParams) { for (let i = 0; i < this.mrcArgs.length; i++) { - const fieldName = 'ARG' + i; const newPort = this.getNewPort(i); ports[newPort] = this.mrcArgs[i].type; } diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index 423b4582..4046a64d 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -230,8 +230,8 @@ export const setup = function () { } export const pythonFromBlock = function ( - block: EventBlock, - generator: ExtendedPythonGenerator) { + _block: EventBlock, + _generator: ExtendedPythonGenerator) { // TODO (Alan): What should this do here?? return ''; } diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index 57fe6889..a80793cb 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -30,7 +30,7 @@ import { getAllowedTypesForSetCheck } from './utils/python'; export const BLOCK_NAME = 'mrc_mechanism'; export const OUTPUT_NAME = 'mrc_mechansim'; -export type ConstructorArg = { +type ConstructorArg = { name: string, type: string, }; @@ -40,7 +40,7 @@ type MechanismExtraState = { params?: ConstructorArg[], } -type MechanismBlock = Blockly.Block & MechanismMixin; +export type MechanismBlock = Blockly.Block & MechanismMixin; interface MechanismMixin extends MechanismMixinType { mrcArgs: ConstructorArg[], mrcImportModule: string, @@ -112,7 +112,7 @@ const MECHANISM = { } } }, - getHardwarePorts: function (this: MechanismBlock, ports: {[key: string]: string}): void { + getHardwarePorts: function (this: MechanismBlock, _ports: {[key: string]: string}): void { // TODO: Collect the hardware ports for this mechanism block that are needed to generate // the robot's define_hardware method. (The key is the port, the value is the type.) }, diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 592d5605..01d83e35 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -31,6 +31,7 @@ import { BLOCK_NAME as MRC_MECHANISM_NAME } from './mrc_mechanism'; import { BLOCK_NAME as MRC_COMPONENT_NAME } from './mrc_component'; import { OUTPUT_NAME as COMPONENT_OUTPUT } from './mrc_component'; import { ComponentBlock } from './mrc_component'; +import { MechanismBlock } from './mrc_mechanism'; import { BLOCK_NAME as MRC_EVENT_NAME } from './mrc_event'; import { OUTPUT_NAME as EVENT_OUTPUT } from './mrc_event'; import { EventBlock } from './mrc_event'; @@ -198,11 +199,8 @@ export const setup = function () { function pythonFromBlockInRobot(block: MechanismComponentHolderBlock, generator: ExtendedPythonGenerator) { let code = 'def define_hardware(self):\n'; - let mechanisms = ''; - let components = ''; - - mechanisms = generator.statementToCode(block, INPUT_MECHANISMS); - components = generator.statementToCode(block, INPUT_COMPONENTS); + const mechanisms = generator.statementToCode(block, INPUT_MECHANISMS); + const components = generator.statementToCode(block, INPUT_COMPONENTS); const body = mechanisms + components; if (body != '') { diff --git a/src/blocks/mrc_port.ts b/src/blocks/mrc_port.ts index 69b748b7..b4dbeab8 100644 --- a/src/blocks/mrc_port.ts +++ b/src/blocks/mrc_port.ts @@ -25,7 +25,6 @@ import { Order } from 'blockly/python'; import { MRC_STYLE_PORTS } from '../themes/styles' import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; -import { createFieldDropdown } from '../fields/FieldDropdown'; export const BLOCK_NAME = 'mrc_port'; export const OUTPUT_NAME = 'mrc_port'; @@ -56,13 +55,12 @@ export const setup = function () { } export const pythonFromBlock = function ( - block: PortBlock, - generator: ExtendedPythonGenerator, -) { - //TODO (Alan) : Specify the type here as well - let code = block.getFieldValue('PORT_NUM'); - - return [code, Order.ATOMIC]; + block: PortBlock, + _generator: ExtendedPythonGenerator) { + // TODO (Alan) : Specify the type here as well + let code = block.getFieldValue('PORT_NUM'); + + return [code, Order.ATOMIC]; } export function createPortShadow(portType: string, portNum: Number) { From f10c546610bb4c563df0d83004150a4a8b2a322a Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Sun, 20 Jul 2025 13:50:45 -0700 Subject: [PATCH 4/4] In robot_base.py: Rename register_event and unregister_event to register_event_handler and unregister_event_handler. Allow multiple handlers to be registered for an event. Add fire_event function. Changed python code for firing an event to call fire_event. When determining whether a workspace has hardware or events, check whether blocks are enabled. --- .../blocks_base_classes/robot_base.py | 28 ++++++++++++--- src/blocks/mrc_call_python_function.ts | 17 +++++++--- src/blocks/mrc_event_handler.ts | 7 +++- src/blocks/mrc_mechanism_component_holder.ts | 8 ++--- .../generated/server_python_scripts.json | 34 +++++++++++++++++-- src/editor/extended_python_generator.ts | 22 ++++++------ src/toolbox/methods_category.ts | 9 ++--- 7 files changed, 94 insertions(+), 31 deletions(-) diff --git a/server_python_scripts/blocks_base_classes/robot_base.py b/server_python_scripts/blocks_base_classes/robot_base.py index 1885caa7..a7080d57 100644 --- a/server_python_scripts/blocks_base_classes/robot_base.py +++ b/server_python_scripts/blocks_base_classes/robot_base.py @@ -5,17 +5,35 @@ class RobotBase: def __init__(self): self.hardware = [] - self.events = {} - def register_event(self, event_name: str, func: Callable) -> None: - self.events[event_name] = func - def unregister_event(self, event_name: str) -> None: - del self.events[event_name] + # In self.event_handlers, the keys are the event names, the values are a list of handlers. + self.event_handlers = {} + + def register_event_handler(self, event_name: str, event_handler: Callable) -> None: + if event_name in self.event_handlers: + self.event_handlers[event_name].append(event_handler) + else: + self.event_handlers[event_name] = [event_handler] + + def unregister_event_handler(self, event_name: str, event_handler: Callable) -> None: + if event_name in self.event_handlers: + if event_handler in self.event_handlers[event_name]: + self.event_handlers[event_name].remove(event_handler) + if not self.event_handlers[event_name]: + del self.event_handlers[event_name] + + def fire_event(self, event_name: str, *args) -> None: + if event_name in self.event_handlers: + for event_handler in self.event_handlers[event_name]: + event_handler(*args) + def start(self) -> None: for hardware in self.hardware: hardware.start() + def update(self) -> None: for hardware in self.hardware: hardware.update() + def stop(self) -> None: for hardware in self.hardware: hardware.stop() diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index b30ac83e..cbbe39c3 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -588,6 +588,8 @@ export const pythonFromBlock = function( generator.addImport(callPythonFunctionBlock.mrcImportModule); } let code; + let needOpenParen = true; + let delimiterBeforeArgs = ''; let argStartIndex = 0; switch (callPythonFunctionBlock.mrcFunctionKind) { case FunctionKind.BUILT_IN: { @@ -637,9 +639,9 @@ export const pythonFromBlock = function( } case FunctionKind.EVENT: { const eventName = block.getFieldValue(FIELD_EVENT_NAME); - code = - 'if self.events.get("' + eventName + '", None):\n' + - generator.INDENT + 'self.events["' + eventName + '"]'; + code = 'self.fire_event("' + eventName + '"'; + needOpenParen = false; + delimiterBeforeArgs = ', '; break; } case FunctionKind.INSTANCE_COMPONENT: { @@ -671,7 +673,14 @@ export const pythonFromBlock = function( default: throw new Error('mrcFunctionKind has unexpected value: ' + callPythonFunctionBlock.mrcFunctionKind) } - code += '(' + generateCodeForArguments(callPythonFunctionBlock, generator, argStartIndex) + ')'; + if (needOpenParen) { + code += '('; + } + const codeForArgs = generateCodeForArguments(callPythonFunctionBlock, generator, argStartIndex); + if (codeForArgs) { + code += delimiterBeforeArgs + codeForArgs; + } + code += ')'; if (block.outputConnection) { return [code, Order.FUNCTION_CALL]; } else { diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index b9c4aacc..920957f1 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -226,6 +226,9 @@ export function pythonFromBlock( const blocklyName = `${sender}_${eventName}`; const funcName = generator.getProcedureName(blocklyName); + // TODO(lizlooney): if the user adds multiple event handlers for the same event + // name, we need to make the event handler function names unique. + let xfix1 = ''; if (generator.STATEMENT_PREFIX) { xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); @@ -321,5 +324,7 @@ function createRobotEventHandlerBlock( // Misc export function getHasEventHandler(workspace: Blockly.Workspace): boolean { - return workspace.getBlocksByType(BLOCK_NAME).length > 0; + return workspace.getBlocksByType(BLOCK_NAME).filter(block => { + return block.isEnabled(); + }).length > 0; } diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 01d83e35..bf98c6ad 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -244,13 +244,13 @@ export const pythonFromBlock = function ( */ export function getHardwarePorts(workspace: Blockly.Workspace, ports: {[key: string]: string}): boolean { let hasHardware = false; - for (const block of workspace.getBlocksByType(BLOCK_NAME)) { + workspace.getBlocksByType(BLOCK_NAME).forEach( block => { const mechanismsInput = block.getInput(INPUT_MECHANISMS); if (mechanismsInput && mechanismsInput.connection) { // Walk through all connected mechanism blocks. let mechanismBlock = mechanismsInput.connection.targetBlock(); while (mechanismBlock) { - if (mechanismBlock.type === MRC_MECHANISM_NAME) { + if (mechanismBlock.type === MRC_MECHANISM_NAME && mechanismBlock.isEnabled()) { hasHardware = true; (mechanismBlock as MechanismBlock).getHardwarePorts(ports); } @@ -263,7 +263,7 @@ export function getHardwarePorts(workspace: Blockly.Workspace, ports: {[key: str // Walk through all connected component blocks. let componentBlock = componentsInput.connection.targetBlock(); while (componentBlock) { - if (componentBlock.type === MRC_COMPONENT_NAME) { + if (componentBlock.type === MRC_COMPONENT_NAME && componentBlock.isEnabled()) { hasHardware = true; (componentBlock as ComponentBlock).getHardwarePorts(ports); } @@ -271,7 +271,7 @@ export function getHardwarePorts(workspace: Blockly.Workspace, ports: {[key: str componentBlock = componentBlock.getNextBlock(); } } - } + }); return hasHardware; } diff --git a/src/blocks/utils/generated/server_python_scripts.json b/src/blocks/utils/generated/server_python_scripts.json index 2f4cec9c..11f81383 100644 --- a/src/blocks/utils/generated/server_python_scripts.json +++ b/src/blocks/utils/generated/server_python_scripts.json @@ -151,12 +151,35 @@ }, { "defaultValue": "", - "name": "func", + "name": "args", + "type": "tuple" + } + ], + "declaringClassName": "blocks_base_classes.RobotBase", + "functionName": "fire_event", + "returnType": "None", + "tooltip": "" + }, + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.RobotBase" + }, + { + "defaultValue": "", + "name": "event_name", + "type": "str" + }, + { + "defaultValue": "", + "name": "event_handler", "type": "typing.Callable" } ], "declaringClassName": "blocks_base_classes.RobotBase", - "functionName": "register_event", + "functionName": "register_event_handler", "returnType": "None", "tooltip": "" }, @@ -197,10 +220,15 @@ "defaultValue": "", "name": "event_name", "type": "str" + }, + { + "defaultValue": "", + "name": "event_handler", + "type": "typing.Callable" } ], "declaringClassName": "blocks_base_classes.RobotBase", - "functionName": "unregister_event", + "functionName": "unregister_event_handler", "returnType": "None", "tooltip": "" }, diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index 3b5db6ab..0be37b49 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -74,11 +74,12 @@ export class ExtendedPythonGenerator extends PythonGenerator { private hasHardware = false; private ports: {[key: string]: string} = Object.create(null); - // Has event handlers (ie, needs to call self.register_events in __init__) + // Has event handlers (ie, needs to call self.register_event_handlers in __init__) private hasEventHandler = false; private classMethods: {[key: string]: string} = Object.create(null); - private events: {[key: string]: {sender: string, eventName: string}} = Object.create(null); + // For eventHandlers, the keys are the function name. + private eventHandlers: {[key: string]: {sender: string, eventName: string}} = Object.create(null); // Opmode details private details : OpModeDetails | null = null; @@ -116,7 +117,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { initStatements += ')\n'; } if (this.hasEventHandler) { - initStatements += this.INDENT + "self.register_events()\n"; + initStatements += this.INDENT + "self.register_event_handlers()\n"; } return initStatements; @@ -175,7 +176,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { } addEventHandler(sender: string, eventName: string, funcName: string): void { - this.events[funcName] = { + this.eventHandlers[funcName] = { 'sender': sender, 'eventName': eventName } @@ -215,18 +216,19 @@ export class ExtendedPythonGenerator extends PythonGenerator { const classDef = 'class ' + className + '(' + simpleBaseClassName + '):\n'; const classMethods = []; - if (this.events && Object.keys(this.events).length > 0) { - let code = 'def register_events(self):\n'; - for (const eventName in this.events) { - const event = this.events[eventName]; - code += this.INDENT + 'self.' + event.sender + '.register_event("' + event.eventName + '", self.' + eventName + ')\n'; + if (this.eventHandlers && Object.keys(this.eventHandlers).length > 0) { + let code = 'def register_event_handlers(self):\n'; + for (const funcName in this.eventHandlers) { + const event = this.eventHandlers[funcName]; + code += this.INDENT + 'self.' + event.sender + '.register_event_handler("' + + event.eventName + '", self.' + funcName + ')\n'; } classMethods.push(code); } for (const name in this.classMethods) { classMethods.push(this.classMethods[name]) } - this.events = Object.create(null); + this.eventHandlers = Object.create(null); this.classMethods = Object.create(null); this.ports = Object.create(null); code = classDef + this.prefixLines(classMethods.join('\n\n'), this.INDENT); diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index 4f3b917b..a7708920 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -68,11 +68,12 @@ export class MethodsCategory { if (this.currentModule) { if (this.currentModule.moduleType == commonStorage.MODULE_TYPE_ROBOT) { // TODO(lizlooney): We need a way to mark a method in python as not overridable. - // For example, in RobotBase, register_event and unregister_event should not be - // overridden in a user's robot. + // For example, in RobotBase, register_event_handler, unregister_event_handler, + // and fire_event should not be overridden in a user's robot. const methodNamesNotOverrideable: string[] = [ - 'register_event', - 'unregister_event', + 'register_event_handler', + 'unregister_event_handler', + 'fire_event', ]; // Add the methods for a Robot. this.addClassBlocksForCurrentModule(