Update wire protocol to version 7 and enhance presence management features
ci / server (push) Failing after 3m44s
ci / client (push) Successful in 15s

- Bumped the wire protocol version to 7, reflecting significant changes in the communication structure.
- Introduced a new presence message type for real-time occupancy updates, including node activity and individual presence states.
- Updated the API to include a directory endpoint for fetching short id→name mappings, improving client-side name resolution.
- Revised the map snapshot structure to be static, with people and current lessons now handled through the presence stream.
- Enhanced client-side handling of presence updates, including UI adjustments to display live occupancy and activity.
- Updated documentation to reflect the new protocol features and changes in presence management.
- Added tests to validate the new presence functionalities and ensure robust handling of real-time data.
This commit is contained in:
Leonid Pershin
2026-08-19 17:00:53 +03:00
parent 4137400621
commit 3c54f981b7
31 changed files with 1025 additions and 373 deletions
@@ -85,6 +85,17 @@ public class ProtocolCodecTests
Assert.Equal(message, ProtocolCodec.ReadSetSpeed(buffer[..length]));
}
[Fact]
public void SkipEmpty_IsASingleByte()
{
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
var length = ProtocolCodec.WriteSkipEmpty(buffer);
Assert.Equal(1, length);
Assert.Equal(MessageType.ClientSkipEmpty, ProtocolCodec.PeekMessageType(buffer[..length]));
}
[Fact]
public void Welcome_RoundTripsAndIsFourBytes()
{
@@ -110,17 +121,44 @@ public class ProtocolCodecTests
}
[Fact]
public void Clock_RoundTripsAndIsFifteenBytes()
public void Clock_RoundTripsAndIsTwentyFourBytes()
{
var message = new ServerClockMessage(7, 1_333_432_800_000, Running: true, SpeedIndex: 2);
var message = new ServerClockMessage(
7,
1_333_432_800_000,
Running: true,
SpeedIndex: 2,
SkipAllowed: true,
SkipTargetUnixMs: 1_333_516_800_000);
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
var length = ProtocolCodec.WriteClock(buffer, message);
Assert.Equal(15, length);
Assert.Equal(24, length);
Assert.Equal((byte)MessageType.ServerClock, buffer[0]);
Assert.Equal(7, BitConverter.ToInt32(buffer[1..5]));
Assert.Equal(1_333_432_800_000, BitConverter.ToInt64(buffer[5..13]));
Assert.Equal(1, buffer[13]);
Assert.Equal(2, buffer[14]);
Assert.Equal(1, buffer[15]);
Assert.Equal(1_333_516_800_000, BitConverter.ToInt64(buffer[16..24]));
Assert.Equal(message, ProtocolCodec.ReadClock(buffer[..length]));
}
[Fact]
public void Clock_WithoutSkip_WritesZeroTarget()
{
var message = new ServerClockMessage(1, 0, Running: false, SpeedIndex: 1);
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
var length = ProtocolCodec.WriteClock(buffer, message);
var read = ProtocolCodec.ReadClock(buffer[..length]);
Assert.Equal(24, length);
Assert.False(read.SkipAllowed);
Assert.Equal(0, read.SkipTargetUnixMs);
}
[Fact]
public void SchoolGone_RoundTripsAndIsFiveBytes()
{
@@ -164,34 +202,45 @@ public class ProtocolCodecTests
Assert.Equal(0, read.Nodes[0].PupilSlots);
Assert.Equal([new MapSnapshotItem("Стул", 2)], read.Nodes[1].Items);
Assert.Equal(["Директор"], read.Nodes[1].Positions);
Assert.Equal("", read.Nodes[0].ActivitySubject);
Assert.Empty(read.Nodes[0].Present);
}
[Fact]
public void MapSnapshot_RoundTripsOccupancyAfterPositions()
public void Presence_RoundTripsAndWritesHeaderOffsets()
{
var message = new ServerMapSnapshotMessage(3, [
new MapSnapshotNode(
3,
"classroom-101",
"floor-1",
"Класс 101",
16,
[new MapSnapshotItem("Парта", 16)],
[],
"Математика",
"5А",
["Иванова Ольга Михайловна", "Соколов Иван Петрович"]),
]);
var buffer = new byte[ProtocolCodec.MapSnapshotSize(message)];
var message = new ServerPresenceMessage(
7,
[
new PresenceNode("classroom-101", 18, "Математика", "5А"),
new PresenceNode("corridor-1", 4),
],
[
new PresencePerson("f0.c0", "classroom-101", PresenceState.Here),
new PresencePerson("f3.p1", "corridor-1", PresenceState.Walking),
]);
var buffer = new byte[ProtocolCodec.PresenceSize(message)];
var length = ProtocolCodec.WriteMapSnapshot(buffer, message);
var read = ProtocolCodec.ReadMapSnapshot(buffer.AsSpan(0, length));
var length = ProtocolCodec.WritePresence(buffer, message);
Assert.Equal((byte)MessageType.ServerPresence, buffer[0]);
Assert.Equal(7, BitConverter.ToInt32(buffer.AsSpan(1, 4)));
Assert.Equal((ushort)2, BitConverter.ToUInt16(buffer.AsSpan(5, 2)));
Assert.Equal(length, ProtocolCodec.PresenceSize(message));
var read = ProtocolCodec.ReadPresence(buffer.AsSpan(0, length));
Assert.Equal(message.SchoolId, read.SchoolId);
Assert.Equal(2, read.Nodes.Count);
Assert.Equal("classroom-101", read.Nodes[0].Id);
Assert.Equal(18, read.Nodes[0].Count);
Assert.Equal("Математика", read.Nodes[0].ActivitySubject);
Assert.Equal("5А", read.Nodes[0].ActivityClass);
Assert.Equal(["Иванова Ольга Михайловна", "Соколов Иван Петрович"], read.Nodes[0].Present);
Assert.Equal("corridor-1", read.Nodes[1].Id);
Assert.Equal(4, read.Nodes[1].Count);
Assert.Equal("", read.Nodes[1].ActivitySubject);
Assert.Equal(2, read.People.Count);
Assert.Equal("f0.c0", read.People[0].Id);
Assert.Equal("classroom-101", read.People[0].NodeId);
Assert.Equal(PresenceState.Here, read.People[0].State);
Assert.Equal(PresenceState.Walking, read.People[1].State);
}
[Fact]
@@ -287,7 +336,9 @@ public class ProtocolCodecTests
{
// Handlers size clock/welcome/pong buffers from this constant; map snapshots use MaxMessageSize.
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
var clock = ProtocolCodec.WriteClock(buffer, new ServerClockMessage(1, long.MaxValue, true, 4));
var clock = ProtocolCodec.WriteClock(
buffer,
new ServerClockMessage(1, long.MaxValue, true, 4, SkipAllowed: true, SkipTargetUnixMs: long.MaxValue));
Assert.True(clock <= ProtocolCodec.MaxFrameSize);
}