Support more PDUs and create Client class

The Client class provides a nice, high-level async API for
client-server communication.
This commit is contained in:
Camden Dixie O'Brien 2025-03-01 10:36:38 +00:00
parent 38d6b2fa9b
commit 9588e88b93
8 changed files with 222 additions and 50 deletions

View File

@ -1,27 +1,6 @@
namespace StudySystemClient { namespace StudySystemClient {
private struct Activity {
public string subject;
public ActivityType type;
}
enum ActivityType {
EXERCISES,
READING;
public string to_string() {
switch (this) {
case EXERCISES:
return "Exercises";
case READING:
return "Reading";
default:
return "Invalid activity type";
}
}
}
public class ActivitiesView : Gtk.Box { public class ActivitiesView : Gtk.Box {
public ActivitiesView(Connection connection) { public ActivitiesView(Client client) {
margin_top = margin_bottom = margin_start = margin_end = 0; margin_top = margin_bottom = margin_start = margin_end = 0;
var scrolled_window = new Gtk.ScrolledWindow(); var scrolled_window = new Gtk.ScrolledWindow();
@ -37,15 +16,15 @@ namespace StudySystemClient {
scrolled_window.add_css_class("card-container"); scrolled_window.add_css_class("card-container");
var activities = new Activity[] { var activities = new Activity[] {
{ "Linguistics", ActivityType.EXERCISES }, { 2, "Linguistics", ActivityType.EXERCISES },
{ "Cybernetics", ActivityType.EXERCISES }, { 1, "Cybernetics", ActivityType.EXERCISES },
{ "Linguistics", ActivityType.READING }, { 2, "Linguistics", ActivityType.READING },
{ "Physics", ActivityType.READING }, { 0, "Physics", ActivityType.READING },
{ "Cybernetics", ActivityType.READING }, { 1, "Cybernetics", ActivityType.READING },
{ "Physics", ActivityType.EXERCISES }, { 0, "Physics", ActivityType.EXERCISES },
}; };
foreach (var activity in activities) { foreach (var activity in activities) {
var card = new ActivityCard(connection, activity); var card = new ActivityCard(client, activity);
card_container.append(card); card_container.append(card);
} }
@ -55,7 +34,7 @@ namespace StudySystemClient {
} }
private class ActivityCard : Gtk.Frame { private class ActivityCard : Gtk.Frame {
public ActivityCard(Connection connection, Activity activity) { public ActivityCard(Client client, Activity activity) {
add_css_class("card"); add_css_class("card");
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 12); var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 12);
@ -63,7 +42,7 @@ namespace StudySystemClient {
var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6); var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6);
text.hexpand = true; text.hexpand = true;
var subject = new Gtk.Label(activity.subject); var subject = new Gtk.Label(activity.subject_name);
subject.halign = Gtk.Align.START; subject.halign = Gtk.Align.START;
subject.add_css_class("activity-subject"); subject.add_css_class("activity-subject");
text.append(subject); text.append(subject);
@ -84,7 +63,7 @@ namespace StudySystemClient {
set_child(content); set_child(content);
var log_session_popover = new LogSessionPopover(connection); var log_session_popover = new LogSessionPopover(client);
log_session_popover.set_parent(button); log_session_popover.set_parent(button);
button.clicked.connect(() => log_session_popover.popup()); button.clicked.connect(() => log_session_popover.popup());
} }
@ -94,10 +73,10 @@ namespace StudySystemClient {
private const int DEFAULT_LENGTH = 30; private const int DEFAULT_LENGTH = 30;
private Gtk.SpinButton input; private Gtk.SpinButton input;
private Connection connection; private Client client;
public LogSessionPopover(Connection connection) { public LogSessionPopover(Client client) {
this.connection = connection; this.client = client;
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
@ -126,8 +105,13 @@ namespace StudySystemClient {
private async void submit() { private async void submit() {
reset(); reset();
popdown(); popdown();
yield connection.send(new Request.Ping());
stderr.printf("Got ACK\n"); try {
yield client.ping();
stderr.printf("Successfully pinged server\n");
} catch (ClientError e) {
stderr.printf("Error pinging server: %s\n", e.message);
}
} }
private void reset() { private void reset() {

24
client/src/activity.vala Normal file
View File

@ -0,0 +1,24 @@
namespace StudySystemClient {
public struct Activity {
public int subject_id;
public string subject_name;
public ActivityType type;
public double priority;
}
public enum ActivityType {
EXERCISES,
READING;
public string to_string() {
switch (this) {
case EXERCISES:
return "Exercises";
case READING:
return "Reading";
default:
return "Invalid activity type";
}
}
}
}

62
client/src/client.vala Normal file
View File

@ -0,0 +1,62 @@
namespace StudySystemClient {
public errordomain ClientError {
ERROR_RESPONSE,
UNEXPECTED_RESPONSE,
}
public class Client {
private Connection connection;
public Client(string cert_dir) throws Error {
connection = new Connection(cert_dir);
}
public async void ping() throws ClientError {
var response = yield connection.send(new Request.Ping());
if (response is Response.Ack) {
return;
} else if (response is Response.Error) {
throw new ClientError.ERROR_RESPONSE(
"Error response to Ping: %s",
response.value.to_string());
} else {
throw new ClientError.UNEXPECTED_RESPONSE(
"Unexpected response to Ping");
}
}
public async Array<Activity> list_activities()
throws ClientError {
var request = new Request.ListActivities();
var response = yield connection.send(request);
if (response is Response.Activities) {
return response.value;
} else if (response is Response.Error) {
throw new ClientError.ERROR_RESPONSE(
"Error response to ListActivities: %s",
response.value.to_string());
} else {
throw new ClientError.UNEXPECTED_RESPONSE(
"Unexpected response to ListActivities");
}
}
public async void log_session(int subject_id, ActivityType type,
int minutes) throws ClientError {
var timestamp = new DateTime.now_utc().to_unix();
var request = new Request.LogSession(subject_id, type,
timestamp, minutes);
var response = yield connection.send(request);
if (response is Response.Ack) {
return;
} else if (response is Response.Error) {
throw new ClientError.ERROR_RESPONSE(
"Error response to LogSession: %s",
response.value.to_string());
} else {
throw new ClientError.UNEXPECTED_RESPONSE(
"Unexpected response to LogSession");
}
}
}
}

View File

@ -14,8 +14,8 @@ namespace StudySystemClient {
css_provider, css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
var connection = new Connection(Config.CERT_DIR); var client = new Client(Config.CERT_DIR);
var main_window = new MainWindow(this, connection); var main_window = new MainWindow(this, client);
main_window.present(); main_window.present();
} catch (Error e) { } catch (Error e) {
stderr.printf("Failed to initialize: %s\n", e.message); stderr.printf("Failed to initialize: %s\n", e.message);

View File

@ -1,22 +1,18 @@
namespace StudySystemClient { namespace StudySystemClient {
public class MainWindow : Gtk.ApplicationWindow { public class MainWindow : Gtk.ApplicationWindow {
private Connection connection; public MainWindow(Gtk.Application app, Client client) {
public MainWindow(Gtk.Application app, Connection connection) {
Object(application: app); Object(application: app);
default_width = 360; default_width = 360;
default_height = 580; default_height = 580;
this.connection = connection;
var header_bar = new Gtk.HeaderBar(); var header_bar = new Gtk.HeaderBar();
var title = new Gtk.Label("Study System Client"); var title = new Gtk.Label("Study System Client");
title.add_css_class("title"); title.add_css_class("title");
header_bar.title_widget = title; header_bar.title_widget = title;
set_titlebar(header_bar); set_titlebar(header_bar);
var activities_view = new ActivitiesView(connection); var activities_view = new ActivitiesView(client);
set_child(activities_view); set_child(activities_view);
} }
} }

View File

@ -10,7 +10,9 @@ configure_file(
lib = library( lib = library(
'study-system-client', 'study-system-client',
sources: files( sources: files(
'activity.vala',
'activities_view.vala', 'activities_view.vala',
'client.vala',
'connection.vala', 'connection.vala',
'der.vala', 'der.vala',
'main_window.vala', 'main_window.vala',

View File

@ -15,16 +15,40 @@ namespace StudySystemClient.Request {
public abstract class Body { public abstract class Body {
protected enum Tag { protected enum Tag {
PING = 0, PING = 0,
LIST_PRIORITIZED_ACTIVITIES = 1, LIST_ACTIVITIES = 1,
LOG_SESSION = 2, LOG_SESSION = 2,
} }
internal Der.Datum datum; internal Der.Datum datum;
protected Body(Tag tag, Der.Datum datum) {
this.datum = new Der.Choice(tag, datum);
}
} }
public class Ping : Body { public class Ping : Body {
public Ping() { public Ping() {
datum = new Der.Choice(Tag.PING, new Der.Null()); base(Tag.PING, new Der.Null());
}
}
public class ListActivities : Body {
public ListActivities() {
base(Tag.LIST_ACTIVITIES, new Der.Null());
}
}
public class LogSession : Body {
public LogSession(int subject_id, ActivityType type,
int64 timestamp, int minutes)
{
var fields = new Der.Datum[] {
new Der.Integer(subject_id),
new Der.Enumerated((int)type),
new Der.Integer(timestamp),
new Der.Integer(minutes),
};
base(Tag.LOG_SESSION, new Der.Sequence(fields));
} }
} }
} }

View File

@ -15,9 +15,9 @@ namespace StudySystemClient.Response {
throw new DecodeError.INVALID_RESPONSE( throw new DecodeError.INVALID_RESPONSE(
"Response was not a SEQUENCE"); "Response was not a SEQUENCE");
} }
if (sequence.value.length != 2) { if (sequence.value.length < 2) {
throw new DecodeError.INVALID_RESPONSE( throw new DecodeError.INVALID_RESPONSE(
"Response sequnce contained %u fields (expected 2)", "Too few fields in Response: %u (expected 2)",
sequence.value.length); sequence.value.length);
} }
var id_datum = sequence.value[0] as Der.Integer; var id_datum = sequence.value[0] as Der.Integer;
@ -68,7 +68,20 @@ namespace StudySystemClient.Response {
public enum Value { public enum Value {
INVALID_REQUEST = 0, INVALID_REQUEST = 0,
INVALID_ARGUMENTS = 1, INVALID_ARGUMENTS = 1,
SERVER_ERROR = 2, SERVER_ERROR = 2;
public string to_string() {
switch (this) {
case INVALID_REQUEST:
return "Invalid request";
case INVALID_ARGUMENTS:
return "Invalid arguments";
case SERVER_ERROR:
return "Server error";
default:
return "Unknown error";
}
}
} }
public Value value { get; private set; } public Value value { get; private set; }
@ -95,4 +108,71 @@ namespace StudySystemClient.Response {
} }
} }
} }
public class Activities : Body {
public Array<Activity> value { get; private set; }
internal Activities.from_datum(Der.Datum datum) throws DecodeError {
value = new Array<Activity>();
if (datum is Der.Sequence) {
foreach (var activity_datum in datum.value)
value.append_val(activity_from_datum(activity_datum));
} else {
throw new DecodeError.INVALID_BODY(
"Activities was not a SEQUENCE");
}
}
private static Activity activity_from_datum(Der.Datum datum)
throws DecodeError {
if (datum is Der.Sequence) {
var fields = datum.value;
if (fields.length < 4) {
throw new DecodeError.INVALID_BODY(
"Too few fields in Activity: %u (expected 4)",
fields.length);
}
var subject_id = get_int("Activity.subjectId", fields[0]);
var subject_name
= get_string("Activity.subjectName", fields[1]);
var activity_type
= get_activity_type("Activity.type", fields[2]);
var int_priority = get_int("Activity.priority", fields[3]);
var priority = (double)int_priority / 100.0;
return { subject_id, subject_name, activity_type, priority };
} else {
throw new DecodeError.INVALID_BODY(
"Activity was not a SEQUENCE");
}
}
private static int get_int(string name, Der.Datum datum)
throws DecodeError {
if (datum is Der.Integer)
return (int)datum.value;
throw new DecodeError.INVALID_BODY(@"$name was not an INTEGER");
}
private static string get_string(string name, Der.Datum datum)
throws DecodeError {
if (datum is Der.Utf8String)
return datum.value;
throw new DecodeError.INVALID_BODY(
@"$name was not a UTF8String");
}
private static ActivityType get_activity_type(
string name, Der.Datum datum) throws DecodeError {
if (datum is Der.Enumerated) {
var value = datum.value;
if (0 <= value <= 1)
return (ActivityType)value;
throw new DecodeError.INVALID_BODY(
"Invalid value for ActivityType: %lld", value);
}
throw new DecodeError.INVALID_BODY(
@"$name was not an ENUMERATED");
}
}
} }