Skip to content

Commit 504ddc0

Browse files
Add maintenance mode
1 parent d0908a0 commit 504ddc0

File tree

11 files changed

+258
-6
lines changed

11 files changed

+258
-6
lines changed

backend/api.test/Mocks/IsarServiceMock.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,15 @@ public async Task ReleaseFromLockdown(string robotIsarUri)
6767
{
6868
await Task.Run(() => Thread.Sleep(1));
6969
}
70+
71+
public async Task SetMaintenanceMode(string robotIsarUri)
72+
{
73+
await Task.Run(() => Thread.Sleep(1));
74+
}
75+
76+
public async Task ReleaseMaintenanceMode(string robotIsarUri)
77+
{
78+
await Task.Run(() => Thread.Sleep(1));
79+
}
7080
}
7181
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using Api.Controllers.Models;
2+
using Api.Services;
3+
using Api.Utilities;
4+
using Microsoft.AspNetCore.Authorization;
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
namespace Api.Controllers
8+
{
9+
[ApiController]
10+
[Route("robots")]
11+
public class MaintenanceModeController(
12+
ILogger<RobotController> logger,
13+
IRobotService robotService,
14+
IIsarService isarService
15+
) : ControllerBase
16+
{
17+
/// <summary>
18+
/// Send the robot to maintenance mode.
19+
/// </summary>
20+
[HttpPost("set-maintenance-mode/{robotId}")]
21+
[Authorize(Roles = Role.User)]
22+
[ProducesResponseType(StatusCodes.Status204NoContent)]
23+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
24+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
25+
[ProducesResponseType(StatusCodes.Status404NotFound)]
26+
[ProducesResponseType(StatusCodes.Status409Conflict)]
27+
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
28+
public async Task<ActionResult> SetMaintenanceMode([FromRoute] string robotId)
29+
{
30+
robotId = Sanitize.SanitizeUserInput(robotId);
31+
32+
var robot = await robotService.ReadById(robotId, readOnly: true);
33+
if (robot is null)
34+
{
35+
logger.LogWarning("Could not find robot with id {Id}", robotId);
36+
return NotFound();
37+
}
38+
39+
try
40+
{
41+
await isarService.SetMaintenanceMode(robot.IsarUri);
42+
}
43+
catch (Exception ex)
44+
{
45+
logger.LogError(ex, "Failed to set maintenance mode for robot {RobotId}", robot.Id);
46+
return StatusCode(StatusCodes.Status500InternalServerError);
47+
}
48+
49+
return NoContent();
50+
}
51+
52+
/// <summary>
53+
/// Release the robot from maintenance mode.
54+
/// </summary>
55+
[HttpPost("release-maintenance-mode/{robotId}")]
56+
[Authorize(Roles = Role.User)]
57+
[ProducesResponseType(StatusCodes.Status204NoContent)]
58+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
59+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
60+
[ProducesResponseType(StatusCodes.Status404NotFound)]
61+
[ProducesResponseType(StatusCodes.Status409Conflict)]
62+
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
63+
public async Task<ActionResult> ReleaseMaintenanceMode([FromRoute] string robotId)
64+
{
65+
robotId = Sanitize.SanitizeUserInput(robotId);
66+
67+
var robot = await robotService.ReadById(robotId, readOnly: true);
68+
if (robot is null)
69+
{
70+
logger.LogWarning("Could not find robot with id {Id}", robotId);
71+
return NotFound();
72+
}
73+
74+
try
75+
{
76+
await isarService.ReleaseMaintenanceMode(robot.IsarUri);
77+
}
78+
catch (Exception ex)
79+
{
80+
logger.LogError(
81+
ex,
82+
"Failed to release maintenance mode for robot {RobotId}",
83+
robot.Id
84+
);
85+
return StatusCode(StatusCodes.Status500InternalServerError);
86+
}
87+
88+
return NoContent();
89+
}
90+
}
91+
}

backend/api/Database/Models/Robot.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public enum RobotStatus
142142
Lockdown,
143143
GoingToLockdown,
144144
GoingToRecharging,
145+
Maintenance,
145146
}
146147

147148
public enum RobotCapabilitiesEnum

backend/api/Services/IsarService.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public interface IIsarService
2626
public Task SendToLockdown(string robotIsarUri);
2727

2828
public Task ReleaseFromLockdown(string robotIsarUri);
29+
30+
public Task SetMaintenanceMode(string robotIsarUri);
31+
32+
public Task ReleaseMaintenanceMode(string robotIsarUri);
2933
}
3034

3135
public class IsarService(IDownstreamApi isarApi, ILogger<IsarService> logger) : IIsarService
@@ -353,6 +357,68 @@ public async Task ReleaseFromLockdown(string robotIsarUri)
353357
}
354358
}
355359

360+
public async Task SetMaintenanceMode(string robotIsarUri)
361+
{
362+
HttpResponseMessage? response;
363+
try
364+
{
365+
response = await CallApi(
366+
HttpMethod.Post,
367+
robotIsarUri,
368+
"schedule/maintenance-mode"
369+
);
370+
}
371+
catch (Exception e)
372+
{
373+
logger.LogError(
374+
"Encountered an exception when making an API call to ISAR: {Message}",
375+
e.Message
376+
);
377+
throw new IsarCommunicationException(e.Message);
378+
}
379+
380+
if (!response.IsSuccessStatusCode)
381+
{
382+
(string message, int statusCode) = GetErrorDescriptionForFailedIsarRequest(
383+
response
384+
);
385+
string errorResponse = await response.Content.ReadAsStringAsync();
386+
logger.LogError("{Message}: {ErrorResponse}", message, errorResponse);
387+
throw new IsarCommunicationException(message);
388+
}
389+
}
390+
391+
public async Task ReleaseMaintenanceMode(string robotIsarUri)
392+
{
393+
HttpResponseMessage? response;
394+
try
395+
{
396+
response = await CallApi(
397+
HttpMethod.Post,
398+
robotIsarUri,
399+
"schedule/release-maintenance-mode"
400+
);
401+
}
402+
catch (Exception e)
403+
{
404+
logger.LogError(
405+
"Encountered an exception when making an API call to ISAR: {Message}",
406+
e.Message
407+
);
408+
throw new IsarCommunicationException(e.Message);
409+
}
410+
411+
if (!response.IsSuccessStatusCode)
412+
{
413+
(string message, int statusCode) = GetErrorDescriptionForFailedIsarRequest(
414+
response
415+
);
416+
string errorResponse = await response.Content.ReadAsStringAsync();
417+
logger.LogError("{Message}: {ErrorResponse}", message, errorResponse);
418+
throw new IsarCommunicationException(message);
419+
}
420+
}
421+
356422
/// <summary>
357423
/// Helper method to call the downstream API
358424
/// </summary>

frontend/src/api/ApiCaller.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,28 @@ export class BackendAPICaller {
387387
return result.content
388388
}
389389

390+
static async setMaintenanceMode(robotId: string) {
391+
const path: string = `robots/set-maintenance-mode/` + robotId
392+
const body = {}
393+
394+
const result = await BackendAPICaller.POST<unknown, unknown>(path, body).catch((e) => {
395+
console.error(`Failed to POST /${path}: ` + e)
396+
throw e
397+
})
398+
return result.content
399+
}
400+
401+
static async releaseMaintenanceMode(robotId: string) {
402+
const path: string = `robots/release-maintenance-mode/` + robotId
403+
const body = {}
404+
405+
const result = await BackendAPICaller.POST<unknown, unknown>(path, body).catch((e) => {
406+
console.error(`Failed to POST /${path}: ` + e)
407+
throw e
408+
})
409+
return result.content
410+
}
411+
390412
static async getInspection(isarInspectionId: string): Promise<Blob> {
391413
const path: string = 'inspection/' + isarInspectionId
392414

frontend/src/components/Displays/RobotDisplays/RobotStatusIcon.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ export const RobotStatusChip = ({ status, isarConnected, itemSize }: StatusProps
8585
status = RobotStatus.Busy
8686
break
8787
}
88+
case RobotStatus.Maintenance: {
89+
statusIcon = Icons.Warning
90+
break
91+
}
8892

8993
default: {
9094
iconColor = tokens.colors.text.static_icons__default.hex

frontend/src/components/Pages/RobotPage/RobotPage.tsx

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const StyledRobotPage = styled(StyledPage)`
2929
background-color: ${tokens.colors.ui.background__light.hex};
3030
gap: 5px;
3131
`
32-
const StyledTextButton = styled(StyledButton)`
32+
const FullWidthButton = styled(StyledButton)`
3333
text-align: left;
3434
align-self: stretch;
3535
`
@@ -117,14 +117,14 @@ export const RobotPage = ({ robotId }: { robotId: string }) => {
117117

118118
const stopButton =
119119
selectedRobot && selectedRobot.status in [RobotStatus.Busy, RobotStatus.Paused] ? (
120-
<StyledTextButton variant="contained" onClick={toggleSkipMissionDialog}>
120+
<FullWidthButton variant="contained" onClick={toggleSkipMissionDialog}>
121121
<Icon
122122
name={Icons.StopButton}
123123
style={{ color: tokens.colors.interactive.icon_on_interactive_colors.rgba }}
124124
size={24}
125125
/>
126126
{TranslateText('Stop')} {selectedRobot.name}
127-
</StyledTextButton>
127+
</FullWidthButton>
128128
) : (
129129
<></>
130130
)
@@ -170,6 +170,12 @@ export const RobotPage = ({ robotId }: { robotId: string }) => {
170170
{selectedRobot && selectedRobot.status == RobotStatus.InterventionNeeded && (
171171
<InterventionNeededButton robot={selectedRobot} />
172172
)}
173+
{selectedRobot && (
174+
<MaintenanceButton
175+
robotId={selectedRobot.id}
176+
robotStatus={selectedRobot.status}
177+
/>
178+
)}
173179
</StyledLeftContent>
174180
<StatusContent>
175181
<StyledStatusElement>
@@ -250,3 +256,48 @@ export const RobotPage = ({ robotId }: { robotId: string }) => {
250256
</>
251257
)
252258
}
259+
260+
interface MaintenanceButtonProps {
261+
robotId: string
262+
robotStatus: RobotStatus
263+
}
264+
265+
const MaintenanceButton = ({ robotId, robotStatus }: MaintenanceButtonProps) => {
266+
const { TranslateText } = useLanguageContext()
267+
const [isStoppingDueToMaintenance, setIsStoppingDueToMaintenance] = useState(false)
268+
269+
const onSetMaintenenceMode = () => {
270+
setIsStoppingDueToMaintenance(true)
271+
272+
BackendAPICaller.setMaintenanceMode(robotId)
273+
.then(() => {
274+
setIsStoppingDueToMaintenance(false)
275+
})
276+
.catch(() => {
277+
console.log(`Unable to set maintenance mode on robot with id ${robotId}. `)
278+
setIsStoppingDueToMaintenance(false)
279+
})
280+
}
281+
282+
const onReleaseMaintenanceMode = () => {
283+
BackendAPICaller.releaseMaintenanceMode(robotId)
284+
.then(() => {})
285+
.catch(() => {
286+
console.log(`Unable to release maintenance mode on robot with id ${robotId}. `)
287+
})
288+
}
289+
290+
return (
291+
<>
292+
{robotStatus == RobotStatus.Maintenance ? (
293+
<FullWidthButton onClick={onReleaseMaintenanceMode}>
294+
{TranslateText(`Release maintenance mode`)}
295+
</FullWidthButton>
296+
) : (
297+
<FullWidthButton color="danger" disabled={isStoppingDueToMaintenance} onClick={onSetMaintenenceMode}>
298+
{TranslateText('Set maintenance mode')}
299+
</FullWidthButton>
300+
)}
301+
</>
302+
)
303+
}

frontend/src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ await msalInstance.initialize()
1414

1515
// Add event callback to prevent multiple interactions
1616
msalInstance.addEventCallback((event) => {
17-
if (event.eventType === EventType.LOGIN_SUCCESS || event.eventType === EventType.ACQUIRE_TOKEN_SUCCESS) {
17+
if (event.eventType === EventType.LOGIN_SUCCESS) {
1818
console.log('Authentication successful')
1919
}
2020
})

frontend/src/language/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,5 +334,8 @@
334334
"ReturnHomePaused": "Returning home paused",
335335
"Current Inspection Area": "Current Inspection Area",
336336
"GoingToRecharging": "Returning to recharge",
337-
"GoingToLockdown": "Returning due to lockdown"
337+
"GoingToLockdown": "Returning due to lockdown",
338+
"Set maintenance mode": "Set maintenance mode",
339+
"Maintenance": "Maintenance",
340+
"Release maintenance mode": "Release maintenance mode"
338341
}

frontend/src/language/no.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,5 +334,8 @@
334334
"ReturnHomePaused": "Kjøring hjem pauset",
335335
"Current Inspection Area": "Nåværende inspeksjonsområde",
336336
"GoingToRecharging": "Returnerer for å lade",
337-
"GoingToLockdown": "Returnerer på grunn av lockdown"
337+
"GoingToLockdown": "Returnerer på grunn av lockdown",
338+
"Set maintenance mode": "Sett vedlikeholdsmodus",
339+
"Maintenance": "Vedlikehold",
340+
"Release maintenance mode": "Frigjør fra vedlikeholdsmodus"
338341
}

0 commit comments

Comments
 (0)