Skip to content

Commit 196fcad

Browse files
hhvrcCopilot
andauthored
Implement binary serialization for backchannel comms (#231)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent e17c456 commit 196fcad

38 files changed

Lines changed: 765 additions & 623 deletions

API/Controller/Shockers/SendControl.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,11 @@
99
using OpenShock.Common.Hubs;
1010
using OpenShock.Common.Models;
1111
using OpenShock.Common.Problems;
12-
using OpenShock.Common.Services.RedisPubSub;
1312

1413
namespace OpenShock.API.Controller.Shockers;
1514

1615
public sealed partial class ShockerController
1716
{
18-
private static readonly IDictionary<string, object> EmptyDic = new Dictionary<string, object>();
19-
2017
/// <summary>
2118
/// Send a control message to shockers
2219
/// </summary>
@@ -31,19 +28,19 @@ public sealed partial class ShockerController
3128
public async Task<IActionResult> SendControl(
3229
[FromBody] ControlRequest body,
3330
[FromServices] IHubContext<UserHub, IUserHub> userHub,
34-
[FromServices] IRedisPubService redisPubService)
31+
[FromServices] IControlSender controlSender)
3532
{
3633
var sender = new ControlLogSender
3734
{
3835
Id = CurrentUser.Id,
3936
Name = CurrentUser.Name,
4037
Image = CurrentUser.GetImageUrl(),
4138
ConnectionId = HttpContext.Connection.Id,
42-
AdditionalItems = EmptyDic,
39+
AdditionalItems = [],
4340
CustomName = body.CustomName
4441
};
4542

46-
var controlAction = await ControlLogic.ControlByUser(body.Shocks, _db, sender, userHub.Clients, redisPubService);
43+
var controlAction = await controlSender.ControlByUser(body.Shocks, sender, userHub.Clients);
4744
return controlAction.Match(
4845
success => LegacyEmptyOk("Successfully sent control messages"),
4946
notFound => Problem(ShockerControlError.ShockerControlNotFound(notFound.Value)),
@@ -65,12 +62,12 @@ public async Task<IActionResult> SendControl(
6562
public Task<IActionResult> SendControl_DEPRECATED(
6663
[FromBody] IReadOnlyList<Common.Models.WebSocket.User.Control> body,
6764
[FromServices] IHubContext<UserHub, IUserHub> userHub,
68-
[FromServices] IRedisPubService redisPubService)
65+
[FromServices] IControlSender controlSender)
6966
{
7067
return SendControl(new ControlRequest
7168
{
7269
Shocks = body,
7370
CustomName = null
74-
}, userHub, redisPubService);
71+
}, userHub, controlSender);
7572
}
7673
}

API/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
using OpenShock.API.Services.Email;
77
using OpenShock.API.Services.UserService;
88
using OpenShock.Common;
9+
using OpenShock.Common.DeviceControl;
910
using OpenShock.Common.Extensions;
1011
using OpenShock.Common.Hubs;
1112
using OpenShock.Common.JsonSerialization;
1213
using OpenShock.Common.Options;
14+
using OpenShock.Common.Services;
1315
using OpenShock.Common.Services.Device;
1416
using OpenShock.Common.Services.LCGNodeProvisioner;
1517
using OpenShock.Common.Services.Ota;
@@ -44,6 +46,7 @@
4446
});
4547

4648
builder.Services.AddScoped<IDeviceService, DeviceService>();
49+
builder.Services.AddScoped<IControlSender, ControlSender>();
4750
builder.Services.AddScoped<IOtaService, OtaService>();
4851
builder.Services.AddScoped<IDeviceUpdateService, DeviceUpdateService>();
4952
builder.Services.AddScoped<IAccountService, AccountService>();

API/Realtime/RedisSubscriberService.cs

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Text.Json;
1+
using MessagePack;
22
using Microsoft.AspNetCore.SignalR;
33
using Microsoft.EntityFrameworkCore;
44
using OpenShock.Common.Hubs;
@@ -9,7 +9,6 @@
99
using OpenShock.Common.Services.RedisPubSub;
1010
using OpenShock.Common.Utils;
1111
using Redis.OM.Contracts;
12-
using Redis.OM.Searching;
1312
using StackExchange.Redis;
1413

1514
namespace OpenShock.API.Realtime;
@@ -23,6 +22,7 @@ public sealed class RedisSubscriberService : IHostedService, IAsyncDisposable
2322
private readonly IDbContextFactory<OpenShockContext> _dbContextFactory;
2423
private readonly IRedisConnectionProvider _redisConnectionProvider;
2524
private readonly ISubscriber _subscriber;
25+
private readonly ILogger<RedisSubscriberService> _logger;
2626

2727
/// <summary>
2828
/// DI Constructor
@@ -31,46 +31,87 @@ public sealed class RedisSubscriberService : IHostedService, IAsyncDisposable
3131
/// <param name="hubContext"></param>
3232
/// <param name="dbContextFactory"></param>
3333
/// <param name="redisConnectionProvider"></param>
34+
/// <param name="logger"></param>
3435
public RedisSubscriberService(
3536
IConnectionMultiplexer connectionMultiplexer,
3637
IHubContext<UserHub, IUserHub> hubContext,
3738
IDbContextFactory<OpenShockContext> dbContextFactory,
38-
IRedisConnectionProvider redisConnectionProvider)
39+
IRedisConnectionProvider redisConnectionProvider,
40+
ILogger<RedisSubscriberService> logger
41+
)
3942
{
4043
_hubContext = hubContext;
4144
_dbContextFactory = dbContextFactory;
4245
_redisConnectionProvider = redisConnectionProvider;
4346
_subscriber = connectionMultiplexer.GetSubscriber();
47+
_logger = logger;
4448
}
4549

4650
/// <inheritdoc />
4751
public async Task StartAsync(CancellationToken cancellationToken)
4852
{
49-
await _subscriber.SubscribeAsync(RedisChannels.KeyEventExpired, (_, message) => { OsTask.Run(() => HandleKeyExpired(message)); });
50-
await _subscriber.SubscribeAsync(RedisChannels.DeviceOnlineStatus, (_, message) => { OsTask.Run(() => HandleDeviceOnlineStatus(message)); });
53+
await _subscriber.SubscribeAsync(RedisChannels.KeyEventExpired, HandleKeyExpired);
54+
await _subscriber.SubscribeAsync(RedisChannels.DeviceStatus, HandleDeviceStatus);
5155
}
5256

53-
private async Task HandleDeviceOnlineStatus(RedisValue message)
57+
private void HandleKeyExpired(RedisChannel _, RedisValue message)
5458
{
5559
if (!message.HasValue) return;
56-
var data = JsonSerializer.Deserialize<DeviceUpdatedMessage>(message.ToString());
57-
if (data is null) return;
60+
if (message.ToString().Split(':', 2) is not [{ } guid, { } name]) return;
61+
62+
if (!Guid.TryParse(guid, out var id)) return;
5863

59-
await LogicDeviceOnlineStatus(data.Id);
64+
if (typeof(DeviceOnline).FullName == name)
65+
{
66+
OsTask.Run(() => LogicDeviceOnlineStatus(id));
67+
}
6068
}
61-
62-
private async Task HandleKeyExpired(RedisValue message)
69+
70+
private void HandleDeviceStatus(RedisChannel _, RedisValue value)
6371
{
64-
if (!message.HasValue) return;
65-
var msg = message.ToString().Split(':');
66-
if (msg.Length < 2) return;
72+
if (!value.HasValue) return;
73+
74+
DeviceStatus message;
75+
try
76+
{
77+
message = MessagePackSerializer.Deserialize<DeviceStatus>(value);
78+
}
79+
catch (Exception e)
80+
{
81+
_logger.LogError(e, "Failed to deserialize redis message");
82+
return;
83+
}
84+
85+
OsTask.Run(() => HandleDeviceStatusMessage(message));
86+
}
6787

88+
private async Task HandleDeviceStatusMessage(DeviceStatus message)
89+
{
90+
switch (message.Payload)
91+
{
92+
case DeviceBoolStatePayload boolState:
93+
await HandleDeviceBoolState(message.DeviceId, boolState);
94+
break;
95+
default:
96+
_logger.LogError("Got DeviceStatus with unknown payload type: {PayloadType}", message.Payload.GetType().Name);
97+
break;
98+
}
6899

69-
if (!Guid.TryParse(msg[1], out var id)) return;
100+
}
70101

71-
if (typeof(DeviceOnline).FullName == msg[0])
102+
private async Task HandleDeviceBoolState(Guid deviceId, DeviceBoolStatePayload state)
103+
{
104+
switch (state.Type)
72105
{
73-
await LogicDeviceOnlineStatus(id);
106+
case DeviceBoolStateType.Online:
107+
await LogicDeviceOnlineStatus(deviceId); // TODO: Handle device offline messages too
108+
break;
109+
case DeviceBoolStateType.EStopped:
110+
_logger.LogInformation("EStopped state not implemented yet for DeviceId {DeviceId}", deviceId);
111+
break;
112+
default:
113+
_logger.LogError("Unknown DeviceBoolStateType: {StateType}", state.Type);
114+
break;
74115
}
75116
}
76117

@@ -111,15 +152,23 @@ await _hubContext.Clients.Users(userIds).DeviceStatus([
111152
}
112153

113154
/// <inheritdoc />
114-
public Task StopAsync(CancellationToken cancellationToken)
155+
public async Task StopAsync(CancellationToken cancellationToken)
115156
{
116-
return Task.CompletedTask;
157+
await _subscriber.UnsubscribeAllAsync();
117158
}
118159

119160
/// <inheritdoc />
120161
public async ValueTask DisposeAsync()
121162
{
122-
await _subscriber.UnsubscribeAllAsync();
163+
try
164+
{
165+
await _subscriber.UnsubscribeAllAsync();
166+
}
167+
catch (Exception ex)
168+
{
169+
_logger.LogError(ex, "Error during Redis unsubscribe in DisposeAsync");
170+
}
171+
123172
GC.SuppressFinalize(this);
124173
}
125174

Common/Common.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PrivateAssets>all</PrivateAssets>
1313
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1414
</PackageReference>
15+
<PackageReference Include="MessagePack" Version="3.1.4" />
1516
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
1617
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.8" />
1718
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.8.0" />

0 commit comments

Comments
 (0)