These were the awkward ones. Each began with an operation packed into a single byte — two of them with a version wedged in beside it — so there was no wrapping them: the layouts had to be rebuilt. The device manager's own enumerate and subscribe become the reserved verbs that mean the same thing everywhere, its replies lose three status structs the envelope already carries, and a device id becomes the packet's target. Power drops the version it repeated on every request, because describe is the handshake, and stops claiming a 64-byte ceiling it never needed for calls. USB moves a control transfer's data to the packet tail in both directions, which makes the status length the transferred length and retires a field that had been saying the same thing twice. The danger in this one was not the protocols but their readers. Init recognised a power button by two bytes at the head of a message, the ACPI service dispatched on the first byte, the xHCI driver read its operation with a raw integer load, and the HID drivers reinterpreted a report wholesale — none of which would have failed to compile once the layouts moved. They would simply have stopped: no shutdown on the power button, no reports from the keyboard. Every one of them now reads through the generated types, and the shutdown gate that answers only a subscriber is the same code it was. Two sizes were decided by measuring rather than assuming. The child-added message is both a request and the event broadcast to subscribers, and alignment rounds it to 48 bytes, which puts its packet exactly on the 64-byte push floor — a test pins that, because a field added carelessly would now overflow it. The interrupt report gives up eight bytes of inline room to make space for the header; the two drivers that produce reports send eight and four. Suite 110/110.
105 lines
5.7 KiB
Zig
105 lines
5.7 KiB
Zig
//! The power protocol (docs/os-development/power.md): system power's
|
|
//! domain-named surface, bound at `/protocol/power`. On x86 the acpi service
|
|
//! provides it; on ARM a PSCI/mailbox service will bind the same name —
|
|
//! subscribers never learn which firmware they are on (docs/discovery.md —
|
|
//! firmware neutrality), which is the whole point of naming the contract rather
|
|
//! than the provider (docs/os-development/protocol-namespace.md).
|
|
//!
|
|
//! Defined through the envelope, so every packet begins with the folded
|
|
//! `Header`. Three shapes ride the channel, and the envelope names all three:
|
|
//!
|
|
//! - **subscribe** is the *reserved* verb, not one of this protocol's own: a
|
|
//! synchronous call whose attached capability is the subscriber's endpoint is
|
|
//! exactly what `envelope.operation_subscribe` means everywhere.
|
|
//! - **shutdown** is this protocol's one verb — the only operation that *does*
|
|
//! something irreversible, and the reason the provider gates it by badge.
|
|
//! - **the events** are pushes: the service `ipc_send`s each one to every
|
|
//! subscriber, no reply owed, so a slow or dead subscriber can never wedge the
|
|
//! source. **The kind is the packet's operation** — one declared event per
|
|
//! named kind, exactly as the input protocol delivers one per device class —
|
|
//! so a subscriber reads *what happened* out of the header instead of a tag
|
|
//! inside the payload. That is what the old `EventMessage`'s two leading bytes
|
|
//! (an operation byte saying "this is an event", then the kind) fold into.
|
|
//!
|
|
//! `Header.target` is unused (0) in both directions: the provider is the only
|
|
//! object either side addresses. And no packet carries a version any more — the
|
|
//! reserved `describe` verb is the version handshake, asked once at connect time
|
|
//! rather than re-carried out of every packet's budget.
|
|
|
|
const std = @import("std");
|
|
const envelope = @import("envelope");
|
|
|
|
/// What a published event carries beyond its kind. The kind is the packet's
|
|
/// operation, so nothing here repeats it; `power_button`, `lid`, `ac` and
|
|
/// `battery` leave both fields zero and are fully described by the verb alone.
|
|
pub const Notice = extern struct {
|
|
/// The device notification code (ACPI `Notify`'s second argument), or 0.
|
|
code: u32 = 0,
|
|
/// The notifying device's hardware id (EISA-decoded), or all zero.
|
|
hid: [8]u8 = .{0} ** 8,
|
|
};
|
|
|
|
pub const Protocol = envelope.Define(.{
|
|
.name = "power",
|
|
.version = 1,
|
|
.operations = &.{
|
|
// Orderly shutdown's last step: enter S5. Honored only from a
|
|
// subscriber — init, the process that has already run the stop sequence
|
|
// over everything else (docs/os-development/power.md, "authority, not
|
|
// information"). Nothing to say and nothing to answer, so the verb and
|
|
// the reply's `Status` are the whole exchange.
|
|
.{ .name = "shutdown" },
|
|
},
|
|
.events = &.{
|
|
// The vocabulary is hardware-neutral: a lid is a lid whether ACPI or a
|
|
// PSCI mailbox reported it. One event per kind, each carrying the same
|
|
// `Notice`, because what differs between them is which thing happened —
|
|
// and that is the header's job now.
|
|
.{ .name = "power_button", .payload = Notice },
|
|
.{ .name = "lid", .payload = Notice },
|
|
.{ .name = "ac", .payload = Notice },
|
|
.{ .name = "battery", .payload = Notice },
|
|
// A device notification that maps to none of the named events — the
|
|
// `code` and `hid` say which device and what happened.
|
|
.{ .name = "notify", .payload = Notice },
|
|
},
|
|
});
|
|
|
|
pub const Operation = Protocol.Operation;
|
|
|
|
/// What happened. The event *is* the kind: this is the generated event
|
|
/// enumeration, re-exported under the name this protocol has always called its
|
|
/// vocabulary, with the same members it has always had.
|
|
pub const Event = Protocol.Event;
|
|
|
|
/// What a provider and a subscriber size their buffers to. This module used to
|
|
/// declare 64 — the *push* floor — which was simply wrong for a protocol whose
|
|
/// requests ride `ipc_call`: a provider sizing its receive buffer to 64 refuses
|
|
/// any caller that sends up to the floor it is entitled to.
|
|
pub const message_maximum: usize = Protocol.message_maximum;
|
|
|
|
test "the kind is the verb, and an event fits the push floor" {
|
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.shutdown));
|
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.power_button));
|
|
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Event.lid));
|
|
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Event.ac));
|
|
try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Event.battery));
|
|
try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Event.notify));
|
|
// subscribe is the RESERVED verb, below the protocol range entirely.
|
|
try std.testing.expectEqual(@as(u32, 2), envelope.operation_subscribe);
|
|
try std.testing.expectEqual(envelope.prefix_size + @sizeOf(Notice), Protocol.event_maximum);
|
|
try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
|
|
// The call floor, not the push floor: `shutdown` is a synchronous call.
|
|
try std.testing.expectEqual(envelope.packet_maximum, message_maximum);
|
|
}
|
|
|
|
test "a pushed event names its kind in the header" {
|
|
var buffer: [envelope.post_maximum]u8 = undefined;
|
|
const packet = Protocol.encodeEvent(.power_button, 0, .{}, &buffer).?;
|
|
try std.testing.expectEqual(Event.power_button, Protocol.eventOf(packet).?);
|
|
|
|
const notified = Protocol.encodeEvent(.notify, 0, .{ .code = 0x80, .hid = "PNP0C0A\x00".* }, &buffer).?;
|
|
try std.testing.expectEqual(Event.notify, Protocol.eventOf(notified).?);
|
|
try std.testing.expectEqual(@as(u32, 0x80), Protocol.decodeEvent(.notify, notified).?.code);
|
|
}
|