|
|
|
///! daemon/gui communication.
|
|
|
|
///! the protocol is a simple TLV construct: MessageTag(u16), length(u64), json-marshalled Message;
|
|
|
|
///! little endian.
|
|
|
|
const std = @import("std");
|
|
|
|
const json = std.json;
|
|
|
|
const mem = std.mem;
|
|
|
|
|
|
|
|
const ByteArrayList = @import("types.zig").ByteArrayList;
|
|
|
|
|
|
|
|
/// common errors returned by read/write functions.
|
|
|
|
pub const Error = error{
|
|
|
|
CommReadInvalidTag,
|
|
|
|
CommReadZeroLenInNonVoidTag,
|
|
|
|
CommWriteTooLarge,
|
|
|
|
};
|
|
|
|
|
|
|
|
/// daemon and gui exchange messages of this type.
|
|
|
|
pub const Message = union(MessageTag) {
|
|
|
|
ping: void,
|
|
|
|
pong: void,
|
|
|
|
poweroff: void,
|
|
|
|
wifi_connect: WifiConnect,
|
|
|
|
network_report: NetworkReport,
|
|
|
|
get_network_report: GetNetworkReport,
|
|
|
|
|
|
|
|
pub const WifiConnect = struct {
|
|
|
|
ssid: []const u8,
|
|
|
|
password: []const u8,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const NetworkReport = struct {
|
|
|
|
ipaddrs: []const []const u8,
|
|
|
|
wifi_ssid: ?[]const u8, // null indicates disconnected from wifi
|
|
|
|
wifi_scan_networks: []const []const u8,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const GetNetworkReport = struct {
|
|
|
|
scan: bool, // true starts a wifi scan and send NetworkReport only after completion
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
/// it is important to preserve ordinal values for future compatiblity,
|
|
|
|
/// especially when nd and gui may temporary diverge in their implementations.
|
|
|
|
pub const MessageTag = enum(u16) {
|
|
|
|
ping = 0x01,
|
|
|
|
pong = 0x02,
|
|
|
|
poweroff = 0x03,
|
|
|
|
wifi_connect = 0x04,
|
|
|
|
network_report = 0x05,
|
|
|
|
get_network_report = 0x06,
|
|
|
|
// next: 0x07
|
|
|
|
};
|
|
|
|
|
|
|
|
/// reads and parses a single message from the input stream reader.
|
|
|
|
/// callers must deallocate resources with free when done.
|
|
|
|
pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
|
|
|
|
// alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic.
|
|
|
|
const tag = reader.readEnum(MessageTag, .Little) catch {
|
|
|
|
return Error.CommReadInvalidTag;
|
|
|
|
};
|
|
|
|
const len = try reader.readIntLittle(u64);
|
|
|
|
if (len == 0) {
|
|
|
|
return switch (tag) {
|
|
|
|
.ping => Message{ .ping = {} },
|
|
|
|
.pong => Message{ .pong = {} },
|
|
|
|
.poweroff => Message{ .poweroff = {} },
|
|
|
|
else => Error.CommReadZeroLenInNonVoidTag,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
var bytes = try allocator.alloc(u8, len);
|
|
|
|
defer allocator.free(bytes);
|
|
|
|
try reader.readNoEof(bytes);
|
|
|
|
const jopt = json.ParseOptions{ .allocator = allocator, .ignore_unknown_fields = true };
|
|
|
|
var jstream = json.TokenStream.init(bytes);
|
|
|
|
return switch (tag) {
|
|
|
|
.ping, .pong, .poweroff => unreachable, // handled above
|
|
|
|
.wifi_connect => Message{
|
|
|
|
.wifi_connect = try json.parse(Message.WifiConnect, &jstream, jopt),
|
|
|
|
},
|
|
|
|
.network_report => Message{
|
|
|
|
.network_report = try json.parse(Message.NetworkReport, &jstream, jopt),
|
|
|
|
},
|
|
|
|
.get_network_report => Message{
|
|
|
|
.get_network_report = try json.parse(Message.GetNetworkReport, &jstream, jopt),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/// outputs the message msg using writer.
|
|
|
|
/// all allocated resources are freed upon return.
|
|
|
|
pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
|
|
|
|
const jopt = .{ .whitespace = null };
|
|
|
|
var data = ByteArrayList.init(allocator);
|
|
|
|
defer data.deinit();
|
|
|
|
switch (msg) {
|
|
|
|
.ping, .pong, .poweroff => {}, // zero length payload
|
|
|
|
.wifi_connect => try json.stringify(msg.wifi_connect, jopt, data.writer()),
|
|
|
|
.network_report => try json.stringify(msg.network_report, jopt, data.writer()),
|
|
|
|
.get_network_report => try json.stringify(msg.get_network_report, jopt, data.writer()),
|
|
|
|
}
|
|
|
|
if (data.items.len > std.math.maxInt(u64)) {
|
|
|
|
return Error.CommWriteTooLarge;
|
|
|
|
}
|
|
|
|
|
|
|
|
try writer.writeIntLittle(u16, @enumToInt(msg));
|
|
|
|
try writer.writeIntLittle(u64, data.items.len);
|
|
|
|
try writer.writeAll(data.items);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn free(allocator: mem.Allocator, m: Message) void {
|
|
|
|
switch (m) {
|
|
|
|
.ping, .pong, .poweroff => {}, // zero length payload
|
|
|
|
else => |v| {
|
|
|
|
json.parseFree(@TypeOf(v), v, .{ .allocator = allocator });
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
test "read" {
|
|
|
|
const t = std.testing;
|
|
|
|
|
|
|
|
var data = std.ArrayList(u8).init(t.allocator);
|
|
|
|
defer data.deinit();
|
|
|
|
const msg = Message{ .wifi_connect = .{ .ssid = "hello", .password = "world" } };
|
|
|
|
try json.stringify(msg.wifi_connect, .{}, data.writer());
|
|
|
|
|
|
|
|
var buf = std.ArrayList(u8).init(t.allocator);
|
|
|
|
defer buf.deinit();
|
|
|
|
try buf.writer().writeIntLittle(u16, @enumToInt(msg));
|
|
|
|
try buf.writer().writeIntLittle(u64, data.items.len);
|
|
|
|
try buf.writer().writeAll(data.items);
|
|
|
|
|
|
|
|
var bs = std.io.fixedBufferStream(buf.items);
|
|
|
|
const res = try read(t.allocator, bs.reader());
|
|
|
|
defer free(t.allocator, res);
|
|
|
|
|
|
|
|
try t.expectEqualStrings(msg.wifi_connect.ssid, res.wifi_connect.ssid);
|
|
|
|
try t.expectEqualStrings(msg.wifi_connect.password, res.wifi_connect.password);
|
|
|
|
}
|
|
|
|
|
|
|
|
test "write" {
|
|
|
|
const t = std.testing;
|
|
|
|
|
|
|
|
var buf = std.ArrayList(u8).init(t.allocator);
|
|
|
|
defer buf.deinit();
|
|
|
|
const msg = Message{ .wifi_connect = .{ .ssid = "wlan", .password = "secret" } };
|
|
|
|
try write(t.allocator, buf.writer(), msg);
|
|
|
|
|
|
|
|
const payload = "{\"ssid\":\"wlan\",\"password\":\"secret\"}";
|
|
|
|
var js = std.ArrayList(u8).init(t.allocator);
|
|
|
|
defer js.deinit();
|
|
|
|
try js.writer().writeIntLittle(u16, @enumToInt(msg));
|
|
|
|
try js.writer().writeIntLittle(u64, payload.len);
|
|
|
|
try js.appendSlice(payload);
|
|
|
|
|
|
|
|
try t.expectEqualStrings(js.items, buf.items);
|
|
|
|
}
|
|
|
|
|
|
|
|
test "write/read void tags" {
|
|
|
|
const t = std.testing;
|
|
|
|
|
|
|
|
var buf = std.ArrayList(u8).init(t.allocator);
|
|
|
|
defer buf.deinit();
|
|
|
|
|
|
|
|
const msg = [_]Message{
|
|
|
|
Message.ping,
|
|
|
|
Message.pong,
|
|
|
|
Message.poweroff,
|
|
|
|
};
|
|
|
|
|
|
|
|
for (msg) |m| {
|
|
|
|
buf.clearAndFree();
|
|
|
|
try write(t.allocator, buf.writer(), m);
|
|
|
|
var bs = std.io.fixedBufferStream(buf.items);
|
|
|
|
const res = try read(t.allocator, bs.reader());
|
|
|
|
free(t.allocator, res); // noop
|
|
|
|
try t.expectEqual(m, res);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
test "msg sequence" {
|
|
|
|
const t = std.testing;
|
|
|
|
|
|
|
|
var buf = std.ArrayList(u8).init(t.allocator);
|
|
|
|
defer buf.deinit();
|
|
|
|
|
|
|
|
const msgs = [_]Message{
|
|
|
|
Message.ping,
|
|
|
|
Message{ .wifi_connect = .{ .ssid = "wlan", .password = "secret" } },
|
|
|
|
Message.pong,
|
|
|
|
Message{ .network_report = .{
|
|
|
|
.ipaddrs = &.{},
|
|
|
|
.wifi_ssid = null,
|
|
|
|
.wifi_scan_networks = &.{ "foo", "bar" },
|
|
|
|
} },
|
|
|
|
};
|
|
|
|
for (msgs) |m| {
|
|
|
|
try write(t.allocator, buf.writer(), m);
|
|
|
|
}
|
|
|
|
|
|
|
|
var bs = std.io.fixedBufferStream(buf.items);
|
|
|
|
for (msgs) |m| {
|
|
|
|
const res = try read(t.allocator, bs.reader());
|
|
|
|
defer free(t.allocator, res);
|
|
|
|
try t.expectEqual(@as(MessageTag, m), @as(MessageTag, res));
|
|
|
|
}
|
|
|
|
}
|