Skip to content

Commit 3edae76

Browse files
committed
Add Multinode support
Bump to version 1.0.0 as it is now feature comparable with its Polyscope 5 sister
1 parent d1dcaa9 commit 3edae76

File tree

11 files changed

+92
-23
lines changed

11 files changed

+92
-23
lines changed

.github/workflows/build-and-test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
working-directory: external-control-backend/tests
2828
run: pytest
2929

30-
build:
30+
build-frontend-and-test:
3131
runs-on: ubuntu-latest
3232

3333
steps:
@@ -65,9 +65,9 @@ jobs:
6565
path: target/external-control-*.urcapx
6666
if-no-files-found: error
6767

68-
deploy:
68+
upload-urcapx:
6969
runs-on: ubuntu-latest
70-
needs: [backend-test, build] # Only runs if both jobs succeed
70+
needs: [backend-test, build-frontend-and-test] # Only runs if both jobs succeed
7171

7272
steps:
7373
- name: Download build artifacts
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
FROM python:3.13-slim
1+
FROM python:3.13-alpine
2+
3+
# # Create necessary directories and set permissions
4+
RUN mkdir -p /tmp && chmod 777 /tmp
25

36
# Install Flask
47
COPY requirements.txt ./
58
RUN pip install --no-cache-dir -r requirements.txt
69

710
# Copy the application into the image
8-
COPY src/* .
11+
COPY src/ ./
912

1013
# Tell Flask where to load the application from
1114
ENV FLASK_APP simple_rest_api.py
1215

1316
# Expose Flask's default port
1417
EXPOSE 5000
1518

16-
# Run the REST service
17-
ENTRYPOINT ["flask", "run", "--host", "0.0.0.0", "--port", "5000"]
19+
# Run the REST service with Flask development server
20+
ENTRYPOINT ["python", "simple_rest_api.py"]
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
# Flask Framework
22
Flask>=2.2
3-
flask-cors>=3.0.10
4-
Werkzeug>=2.2
3+
flask-cors>=3.0.10

external-control-backend/src/simple_rest_api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,28 @@
44
import request_program
55
import socket
66
import time
7+
import logging
8+
import sys
9+
10+
# Configure logging
11+
logging.basicConfig(
12+
level=logging.INFO,
13+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
14+
stream=sys.stdout
15+
)
16+
logger = logging.getLogger(__name__)
717

818
command = "request_program\n"
919

1020
# Create a simple rest api with Flask (https://flask.palletsprojects.com/en/2.0.x/)
1121
app = Flask(__name__)
1222
CORS(app)
1323

24+
# Log startup message
25+
logger.info("Starting Flask application...")
26+
logger.info(f"Server hostname: {socket.gethostname()}")
27+
logger.info(f"Server IP: {socket.gethostbyname(socket.gethostname())}")
28+
1429
# Simple in-memory cache: {(port, robotIP): (timestamp, program)}
1530
program_cache = {}
1631
CACHE_TTL = 2 # seconds
@@ -55,21 +70,30 @@ def store_in_cache(cache_key, now, json_str, valid):
5570

5671
@app.route('/<int:port>/<robotIP>/', methods=["GET"])
5772
def read_params(port, robotIP):
73+
logger.info(f"Received request for port {port} and robot IP {robotIP}")
5874
cache_key = (port, robotIP)
5975
now = time.time()
6076
cached_resp = get_cached_response(cache_key, now)
6177
if cached_resp:
78+
logger.info(f"Returning cached response for port {port} and robot IP {robotIP}")
6279
return cached_resp
6380
status = None
6481
try:
82+
logger.info(f"Connecting to robot at {robotIP}:{port}")
6583
con = request_program.RequestProgram(port, robotIP)
6684
program = con.send_command(command)
6785
valid = bool(program and program.strip())
6886
status = "ok"
87+
logger.info(f"Successfully retrieved program from robot at {robotIP}:{port}")
6988
except Exception as e:
7089
program = ''
7190
valid = False
7291
status = str(e)
92+
logger.error(f"Error connecting to robot at {robotIP}:{port}: {str(e)}")
7393
json_str = build_json_response(program, valid, status)
7494
store_in_cache(cache_key, now, json_str, valid)
7595
return flask.Response(json_str, mimetype='application/json')
96+
97+
if __name__ == '__main__':
98+
logger.info("Flask application is ready to serve requests")
99+
app.run(host='0.0.0.0', port=5000)

external-control-frontend/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

external-control-frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"generatepath": "generate-manifest-constants ../manifest.yaml"
1212
},
1313
"private": true,
14-
"license": "Apache-2.0",
14+
"license": "BSD-3-Clause",
1515
"dependencies": {
1616
"@angular/animations": "17.1.2",
1717
"@angular/cdk": "17.1.2",

external-control-frontend/src/app/components/external-control-program/external-control-program.behavior.worker.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { ExternalControlProgramNode } from './external-control-program.node';
1717
import { URCAP_ID, VENDOR_ID } from 'src/generated/contribution-constants';
1818
import { ExternalControlApplicationNode } from '../external-control-application/external-control-application.node';
19+
import { v4 as uuidv4 } from 'uuid';
1920

2021
const createProgramNodeLabel = async (node: ExternalControlProgramNode): Promise<AdvancedTranslatedProgramLabel> => {
2122
const api = new ProgramBehaviorAPI(self);
@@ -38,22 +39,35 @@ const createProgramNode = async (): Promise<ExternalControlProgramNode> => {
3839
const applicationNode = await api.applicationService.getApplicationNode('universal-robots-external-control-external-control-application') as ExternalControlApplicationNode;
3940
return ({
4041
type: 'universal-robots-external-control-external-control-program',
41-
version: '1.0.0',
42+
version: '1.1.0',
4243
lockChildren: false,
4344
allowsChildren: false,
45+
parameters: {
46+
nodeHash: uuidv4()
47+
}
4448
});
4549
};
4650

4751
export const fetchBackendJson = async (port: number, robotIP: string, api: ProgramBehaviorAPI): Promise<any> => {
4852
const url = api.getContainerContributionURL(VENDOR_ID, URCAP_ID, 'external-control-backend', 'rest-api');
4953
const backendUrl = `${location.protocol}//${url}/${port}/${robotIP}`;
54+
console.log("backendUrl", backendUrl);
5055
const response = await fetch(backendUrl);
56+
console.log("response", response);
5157
if (!response.ok) {
5258
throw new Error(`Backend error: ${response.status}`);
5359
}
5460
return await response.json();
5561
};
5662

63+
export const isThisNodeTheFirstUrcapNodeInTree = async (node: ExternalControlProgramNode, api: ProgramBehaviorAPI): Promise<boolean> => {
64+
const listOfUrcapNodesInTree = (await api.programTreeService.getContributedNodeInstancesForURCap())
65+
const filteredListOfUrcapNodesInTree = listOfUrcapNodesInTree.filter(node => !node.node.isSuppressed && node.node.type === 'universal-robots-external-control-external-control-program')
66+
const sortedListOfUrcapNodesInTree = filteredListOfUrcapNodesInTree.sort((a, b) => a.node.parameters?.nodeHash.localeCompare(b.node.parameters.nodeHash))
67+
68+
return sortedListOfUrcapNodesInTree.length > 0 && sortedListOfUrcapNodesInTree[0].node.parameters?.nodeHash === node.parameters?.nodeHash;
69+
}
70+
5771
const generateScriptCodeBefore = async (node: ExternalControlProgramNode, ScriptContext: ScriptContext): Promise<ScriptBuilder> => {
5872
const api = new ProgramBehaviorAPI(self);
5973
const applicationNode = await api.applicationService.getApplicationNode('universal-robots-external-control-external-control-application') as ExternalControlApplicationNode;
@@ -73,17 +87,26 @@ const generateScriptCodeBefore = async (node: ExternalControlProgramNode, Script
7387
const generateScriptCodeAfter = (node: ExternalControlProgramNode): OptionalPromise<ScriptBuilder> => new ScriptBuilder();
7488

7589
const generatePreambleScriptCode = async (node: ExternalControlProgramNode, ScriptContext: ScriptContext): Promise<ScriptBuilder> => {
90+
console.log('generatePreambleScriptCode');
7691
const api = new ProgramBehaviorAPI(self);
77-
const applicationNode = await api.applicationService.getApplicationNode('universal-robots-external-control-external-control-application') as ExternalControlApplicationNode;
78-
const port = applicationNode.port;
79-
const robotIP = applicationNode.robotIP;
92+
8093
const builder = new ScriptBuilder();
81-
try {
82-
const json = await fetchBackendJson(port, robotIP, api);
83-
builder.addRaw(json.preamble || '');
84-
} catch (e) {
94+
95+
if (await isThisNodeTheFirstUrcapNodeInTree(node, api)) {
96+
// Fetch the preamble from the backend
97+
const applicationNode = await api.applicationService.getApplicationNode('universal-robots-external-control-external-control-application') as ExternalControlApplicationNode;
98+
const port = applicationNode.port;
99+
const robotIP = applicationNode.robotIP;
100+
try {
101+
const json = await fetchBackendJson(port, robotIP, api);
102+
builder.addRaw(json.preamble || '');
103+
} catch (e) {
104+
builder.addRaw('');
105+
}
106+
} else {
85107
builder.addRaw('');
86108
}
109+
87110
return builder;
88111
};
89112

@@ -105,7 +128,21 @@ const allowChildInsert = (node: ProgramNode, childType: string): OptionalPromise
105128

106129
const allowedInsert = (insertionContext: InsertionContext): OptionalPromise<boolean> => true;
107130

108-
const nodeUpgrade = (loadedNode: ProgramNode): ProgramNode => loadedNode;
131+
const nodeUpgrade = (loadedNode: ProgramNode): ProgramNode => {
132+
const upgradedNode = { ...loadedNode };
133+
134+
// Ensure parameters object exists
135+
if (!upgradedNode.parameters) {
136+
upgradedNode.parameters = {};
137+
}
138+
139+
// Add nodeHash if missing
140+
if (!upgradedNode.parameters.nodeHash) {
141+
upgradedNode.parameters.nodeHash = uuidv4();
142+
}
143+
144+
return upgradedNode;
145+
};
109146

110147
const behaviors: ProgramBehaviors = {
111148
programNodeLabel: createProgramNodeLabel,

external-control-frontend/src/app/components/external-control-program/external-control-program.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class ExternalControlProgramComponent implements OnChanges,
4747
this.cd.detectChanges();
4848
});
4949
}
50-
if (changes?.presenterAPI.isFirstChange() && this.presenterAPI) {
50+
if (changes?.presenterAPI?.firstChange && this.presenterAPI) {
5151
const applicationNode =
5252
await this.presenterAPI.applicationService.getApplicationNode(
5353
'universal-robots-external-control-external-control-application') as

external-control-frontend/src/app/components/external-control-program/external-control-program.node.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ export interface ExternalControlProgramNode extends ProgramNode {
44
type: 'universal-robots-external-control-external-control-program';
55
lockChildren?: boolean;
66
allowsChildren?: boolean;
7+
parameters: {
8+
nodeHash: string;
9+
}
710
}

manifest.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ metadata:
33
urcapID: external-control
44
vendorName: Universal Robots
55
urcapName: "external-control"
6-
version: 0.1.0
6+
version: 1.0.0
77
contactInfo: Energivej 51, 5260 Odense S, Denmark
88
description: Enable External Control for e.g. the ROS2 driver
99
copyright: Copyright (c) 2024 Universal Robots. All rights reserved.

0 commit comments

Comments
 (0)