feat(cardputerzero): record Linux session contracts

This commit is contained in:
Trail Mate Dev
2026-06-02 11:21:16 +08:00
parent 99f37635ce
commit bda67d17dc
14 changed files with 433 additions and 6 deletions
@@ -23,6 +23,20 @@ framebuffer and evdev owner are promoted into this shell.
- LoRa: SX1262 on `/dev/spidev0.1`, 500000 Hz, Reset=26, IRQ/DIO1=23,
Busy=22, DIO2 RF switch enabled, DIO3 TCXO enabled
## Linux Session Component Facts
- notifications: standard `org.freedesktop.Notifications` D-Bus provider
supplied by the Cardputer Zero user session
- notification shell IPC: `$XDG_RUNTIME_DIR/cardputer-zero/notifyd.sock`,
status/control only, never a notification creation path
- input method: Fcitx5 text input through the normal Linux frontend path
- IME display bridge: `cardputerzero-ui` Fcitx5 UI addon exports panel state to
`$XDG_RUNTIME_DIR/cardputer-zero/ime-panel.sock`
- IME panel: `cardputer-zero-ime-panel` renders candidate/preedit state as a
Wayland layer-shell projection and does not submit text
- expected session environment: `XMODIFIERS=@im=fcitx`,
`SDL_IM_MODULE=fcitx`
## Build Entrypoint
- `builds/linux_cmake`
@@ -37,6 +51,9 @@ May:
- own future framebuffer, evdev, packaging, and launch details for this device
- own future Trail Mate Linux SX1262 packet-radio wiring for the documented
Cardputer Zero LoRa endpoint
- route application notifications through the standard freedesktop notification
contract
- respect the Cardputer Zero Fcitx5 user-session boundary for text input
Must not:
@@ -45,6 +62,12 @@ Must not:
- define protocol, chat, map, or storage semantics
- absorb shared Linux runtime code that belongs in `platform/linux/common`
- hide missing real-device hardware integration behind simulator naming
- use Cardputer Zero notifyd shell IPC to create notifications
- embed or fork the Cardputer Zero notifyd store, toast, or notification-center
implementation
- turn the IME panel socket into a text submission path
- embed the Cardputer Zero Fcitx5 addon, panel renderer, input method engine,
dictionary, or candidate selection policy inside Trail Mate
## Thin App Shell Entrypoint Declaration
+2
View File
@@ -12,6 +12,8 @@ include("${TRAIL_MATE_REPO_ROOT}/cmake/TrailMateUxPacks.cmake")
trailmate_add_ui_lvgl_ux_packs(trailmate_ui_lvgl_ux_packs)
add_library(trailmate_linux_cardputer_zero_shell
src/cardputer_zero_input_method_port.cpp
src/cardputer_zero_notification_port.cpp
src/linux_cardputer_zero_app_shell.cpp
"${TRAIL_MATE_REPO_ROOT}/modules/product_composition/src/target_profile.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/product_composition/src/target_ux_binding.cpp")
+15
View File
@@ -17,4 +17,19 @@ Known hardware facts in this repo:
- LoRa: SX1262 on `/dev/spidev0.1`, 500000 Hz, Reset=26, IRQ/DIO1=23,
Busy=22, DIO2 RF switch and DIO3 TCXO enabled
Known Cardputer Zero Linux session facts in this shell:
- notifications are created through the standard
`org.freedesktop.Notifications` D-Bus interface; the
`$XDG_RUNTIME_DIR/cardputer-zero/notifyd.sock` channel is status/control only
- text entry uses Fcitx5 through the normal Linux toolkit/frontend path
- the Cardputer Zero IME addon/panel pair uses
`$XDG_RUNTIME_DIR/cardputer-zero/ime-panel.sock` only to display preedit,
candidates, and input-method state
- Trail Mate does not own the notification daemon implementation, Fcitx5 UI
addon, IME panel renderer, input method engine, dictionaries, or text commit
path
- the shared embedded touch/pinyin IME is not the Cardputer Zero Linux input
method strategy
The simulator remains a separate development shell under `apps/linux_sim_shell`.
@@ -0,0 +1,82 @@
#include "cardputer_zero_input_method_port.h"
#include <cstring>
namespace trailmate
{
namespace apps
{
namespace linux_cardputer_zero
{
namespace
{
constexpr CardputerZeroInputMethodContract kContract = {
"fcitx5",
"Fcitx5 -> Linux toolkit frontend -> application",
"cardputerzero-ui",
"libcardputerzero-ui",
"cardputer-zero-ime-panel",
"cardputer-zero-ime-session",
"cardputer-zero-ime.service",
"cardputer-zero/ime-panel.sock",
"unix-jsonl-addon-to-panel",
"panel.update",
"panel.hide",
"state.update",
"@im=fcitx",
"fcitx",
"Control+space",
320,
170,
3,
false,
false,
false,
false,
false,
false,
false,
};
} // namespace
const CardputerZeroInputMethodContract& CardputerZeroInputMethodPort::contract() const
{
return kContract;
}
bool CardputerZeroInputMethodPort::validate() const
{
const auto& c = contract();
return std::strcmp(c.framework_name, "fcitx5") == 0 &&
std::strcmp(c.text_submission_path,
"Fcitx5 -> Linux toolkit frontend -> application") == 0 &&
std::strcmp(c.ui_addon_name, "cardputerzero-ui") == 0 &&
std::strcmp(c.ui_addon_library, "libcardputerzero-ui") == 0 &&
std::strcmp(c.panel_executable, "cardputer-zero-ime-panel") == 0 &&
std::strcmp(c.session_executable, "cardputer-zero-ime-session") == 0 &&
std::strcmp(c.user_service, "cardputer-zero-ime.service") == 0 &&
std::strcmp(c.panel_socket_suffix, "cardputer-zero/ime-panel.sock") == 0 &&
std::strcmp(c.panel_transport, "unix-jsonl-addon-to-panel") == 0 &&
std::strcmp(c.panel_update_message, "panel.update") == 0 &&
std::strcmp(c.panel_hide_message, "panel.hide") == 0 &&
std::strcmp(c.state_update_message, "state.update") == 0 &&
std::strcmp(c.xmodifiers, "@im=fcitx") == 0 &&
std::strcmp(c.sdl_im_module, "fcitx") == 0 &&
std::strcmp(c.trigger_key, "Control+space") == 0 &&
c.screen_width == 320 &&
c.screen_height == 170 &&
c.max_exported_candidates == 3 &&
!c.app_owns_input_method_engine &&
!c.app_owns_candidate_display &&
!c.app_owns_text_commit &&
!c.panel_socket_commits_text &&
!c.panel_requests_keyboard_focus &&
!c.missing_panel_blocks_text_input &&
!c.uses_embedded_touch_ime;
}
} // namespace linux_cardputer_zero
} // namespace apps
} // namespace trailmate
@@ -0,0 +1,48 @@
#pragma once
namespace trailmate
{
namespace apps
{
namespace linux_cardputer_zero
{
struct CardputerZeroInputMethodContract
{
const char* framework_name;
const char* text_submission_path;
const char* ui_addon_name;
const char* ui_addon_library;
const char* panel_executable;
const char* session_executable;
const char* user_service;
const char* panel_socket_suffix;
const char* panel_transport;
const char* panel_update_message;
const char* panel_hide_message;
const char* state_update_message;
const char* xmodifiers;
const char* sdl_im_module;
const char* trigger_key;
int screen_width;
int screen_height;
int max_exported_candidates;
bool app_owns_input_method_engine;
bool app_owns_candidate_display;
bool app_owns_text_commit;
bool panel_socket_commits_text;
bool panel_requests_keyboard_focus;
bool missing_panel_blocks_text_input;
bool uses_embedded_touch_ime;
};
class CardputerZeroInputMethodPort
{
public:
const CardputerZeroInputMethodContract& contract() const;
bool validate() const;
};
} // namespace linux_cardputer_zero
} // namespace apps
} // namespace trailmate
@@ -0,0 +1,107 @@
#include "cardputer_zero_notification_port.h"
#include <cstring>
namespace trailmate
{
namespace apps
{
namespace linux_cardputer_zero
{
namespace
{
constexpr FreedesktopNotificationsContract kContract = {
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
"Notify",
"susssasa{sv}i",
"u",
"CloseNotification",
"GetCapabilities",
"GetServerInformation",
"NotificationClosed",
"ActionInvoked",
"body",
"actions",
"persistence",
"urgency",
"desktop-entry",
"transient",
"category",
"resident",
"cardputer-zero/notifyd.sock",
false,
};
const char* safeString(const char* value)
{
return value != nullptr ? value : "";
}
std::uint8_t urgencyHint(NotificationUrgency urgency)
{
switch (urgency)
{
case NotificationUrgency::Low:
return 0;
case NotificationUrgency::Critical:
return 2;
case NotificationUrgency::Normal:
default:
return 1;
}
}
} // namespace
const FreedesktopNotificationsContract& CardputerZeroNotificationPort::contract() const
{
return kContract;
}
FreedesktopNotifyCall
CardputerZeroNotificationPort::makeNotifyCall(const NotificationRequest& request) const
{
FreedesktopNotifyCall call;
call.contract = &contract();
call.app_name = safeString(request.app_name);
call.replaces_id = request.replaces_id;
call.app_icon = safeString(request.app_icon);
call.summary = safeString(request.summary);
call.body = safeString(request.body);
call.urgency_hint = urgencyHint(request.urgency);
call.expire_timeout_ms = request.expire_timeout_ms;
return call;
}
bool CardputerZeroNotificationPort::validate() const
{
const auto& c = contract();
return std::strcmp(c.bus_name, "org.freedesktop.Notifications") == 0 &&
std::strcmp(c.object_path, "/org/freedesktop/Notifications") == 0 &&
std::strcmp(c.interface_name, "org.freedesktop.Notifications") == 0 &&
std::strcmp(c.notify_method, "Notify") == 0 &&
std::strcmp(c.notify_signature, "susssasa{sv}i") == 0 &&
std::strcmp(c.notify_return_signature, "u") == 0 &&
std::strcmp(c.close_method, "CloseNotification") == 0 &&
std::strcmp(c.get_capabilities_method, "GetCapabilities") == 0 &&
std::strcmp(c.get_server_information_method, "GetServerInformation") == 0 &&
std::strcmp(c.notification_closed_signal, "NotificationClosed") == 0 &&
std::strcmp(c.action_invoked_signal, "ActionInvoked") == 0 &&
std::strcmp(c.capability_body, "body") == 0 &&
std::strcmp(c.capability_actions, "actions") == 0 &&
std::strcmp(c.capability_persistence, "persistence") == 0 &&
std::strcmp(c.hint_urgency, "urgency") == 0 &&
std::strcmp(c.hint_desktop_entry, "desktop-entry") == 0 &&
std::strcmp(c.hint_transient, "transient") == 0 &&
std::strcmp(c.hint_category, "category") == 0 &&
std::strcmp(c.hint_resident, "resident") == 0 &&
std::strcmp(c.shell_ipc_socket_suffix, "cardputer-zero/notifyd.sock") == 0 &&
!c.shell_ipc_creates_notifications;
}
} // namespace linux_cardputer_zero
} // namespace apps
} // namespace trailmate
@@ -0,0 +1,77 @@
#pragma once
#include <cstdint>
namespace trailmate
{
namespace apps
{
namespace linux_cardputer_zero
{
enum class NotificationUrgency : std::uint8_t
{
Low = 0,
Normal = 1,
Critical = 2,
};
struct NotificationRequest
{
const char* app_name = "Trail Mate";
const char* summary = "";
const char* body = "";
const char* app_icon = "";
std::uint32_t replaces_id = 0;
NotificationUrgency urgency = NotificationUrgency::Normal;
int expire_timeout_ms = -1;
};
struct FreedesktopNotificationsContract
{
const char* bus_name;
const char* object_path;
const char* interface_name;
const char* notify_method;
const char* notify_signature;
const char* notify_return_signature;
const char* close_method;
const char* get_capabilities_method;
const char* get_server_information_method;
const char* notification_closed_signal;
const char* action_invoked_signal;
const char* capability_body;
const char* capability_actions;
const char* capability_persistence;
const char* hint_urgency;
const char* hint_desktop_entry;
const char* hint_transient;
const char* hint_category;
const char* hint_resident;
const char* shell_ipc_socket_suffix;
bool shell_ipc_creates_notifications;
};
struct FreedesktopNotifyCall
{
const FreedesktopNotificationsContract* contract = nullptr;
const char* app_name = "";
std::uint32_t replaces_id = 0;
const char* app_icon = "";
const char* summary = "";
const char* body = "";
std::uint8_t urgency_hint = 1;
int expire_timeout_ms = -1;
};
class CardputerZeroNotificationPort
{
public:
const FreedesktopNotificationsContract& contract() const;
FreedesktopNotifyCall makeNotifyCall(const NotificationRequest& request) const;
bool validate() const;
};
} // namespace linux_cardputer_zero
} // namespace apps
} // namespace trailmate
@@ -47,6 +47,16 @@ LinuxCardputerZeroAppShell::boardFacts() const
return boards::cardputerzero::kBoardFacts;
}
const CardputerZeroNotificationPort& LinuxCardputerZeroAppShell::notificationPort() const
{
return notification_port_;
}
const CardputerZeroInputMethodPort& LinuxCardputerZeroAppShell::inputMethodPort() const
{
return input_method_port_;
}
bool LinuxCardputerZeroAppShell::validate() const
{
const auto& facts = boardFacts();
@@ -76,7 +86,9 @@ bool LinuxCardputerZeroAppShell::validate() const
product_composition::findTargetUxBinding(targetId()) != nullptr &&
config_.ux_pack_id != nullptr &&
std::strcmp(config_.ux_pack_id, activeUxPackId()) == 0 &&
ui_lvgl_ux::findUxPackById(activeUxPackId()) != nullptr;
ui_lvgl_ux::findUxPackById(activeUxPackId()) != nullptr &&
notificationPort().validate() &&
inputMethodPort().validate();
}
} // namespace linux_cardputer_zero
@@ -1,6 +1,8 @@
#pragma once
#include "boards/cardputerzero/board_facts.h"
#include "cardputer_zero_input_method_port.h"
#include "cardputer_zero_notification_port.h"
#include "product_composition/target_profile.h"
@@ -28,10 +30,14 @@ class LinuxCardputerZeroAppShell
const product_composition::TargetProfile* targetProfile() const;
const char* activeUxPackId() const;
const boards::cardputerzero::CardputerZeroBoardFacts& boardFacts() const;
const CardputerZeroNotificationPort& notificationPort() const;
const CardputerZeroInputMethodPort& inputMethodPort() const;
bool validate() const;
private:
LinuxCardputerZeroAppShellConfig config_{};
CardputerZeroNotificationPort notification_port_{};
CardputerZeroInputMethodPort input_method_port_{};
};
} // namespace linux_cardputer_zero
@@ -38,6 +38,49 @@ int main()
assert(facts.lora_dio2_as_rf_switch);
assert(facts.lora_dio3_tcxo_voltage);
const auto& notifications = shell.notificationPort().contract();
assert(std::strcmp(notifications.bus_name, "org.freedesktop.Notifications") == 0);
assert(std::strcmp(notifications.object_path, "/org/freedesktop/Notifications") == 0);
assert(std::strcmp(notifications.notify_signature, "susssasa{sv}i") == 0);
assert(std::strcmp(notifications.capability_body, "body") == 0);
assert(std::strcmp(notifications.capability_actions, "actions") == 0);
assert(std::strcmp(notifications.capability_persistence, "persistence") == 0);
assert(std::strcmp(notifications.shell_ipc_socket_suffix,
"cardputer-zero/notifyd.sock") == 0);
assert(!notifications.shell_ipc_creates_notifications);
trailmate::apps::linux_cardputer_zero::NotificationRequest notification_request;
notification_request.summary = "Mesh";
notification_request.body = "Message received";
notification_request.urgency =
trailmate::apps::linux_cardputer_zero::NotificationUrgency::Critical;
const auto notify_call = shell.notificationPort().makeNotifyCall(notification_request);
assert(notify_call.contract == &notifications);
assert(std::strcmp(notify_call.summary, "Mesh") == 0);
assert(std::strcmp(notify_call.body, "Message received") == 0);
assert(notify_call.urgency_hint == 2);
const auto& input_method = shell.inputMethodPort().contract();
assert(std::strcmp(input_method.framework_name, "fcitx5") == 0);
assert(std::strcmp(input_method.ui_addon_name, "cardputerzero-ui") == 0);
assert(std::strcmp(input_method.panel_socket_suffix,
"cardputer-zero/ime-panel.sock") == 0);
assert(std::strcmp(input_method.panel_update_message, "panel.update") == 0);
assert(std::strcmp(input_method.panel_hide_message, "panel.hide") == 0);
assert(std::strcmp(input_method.state_update_message, "state.update") == 0);
assert(std::strcmp(input_method.xmodifiers, "@im=fcitx") == 0);
assert(std::strcmp(input_method.sdl_im_module, "fcitx") == 0);
assert(input_method.screen_width == 320);
assert(input_method.screen_height == 170);
assert(input_method.max_exported_candidates == 3);
assert(!input_method.app_owns_input_method_engine);
assert(!input_method.app_owns_candidate_display);
assert(!input_method.app_owns_text_commit);
assert(!input_method.panel_socket_commits_text);
assert(!input_method.panel_requests_keyboard_focus);
assert(!input_method.missing_panel_blocks_text_input);
assert(!input_method.uses_embedded_touch_ime);
const auto* profile = shell.targetProfile();
assert(profile != nullptr);
assert(profile->platform == product_composition::TargetPlatform::Linux);
+6
View File
@@ -26,6 +26,11 @@ This record describes current repo evidence for the Linux Cardputer Zero route.
- LoRa SPI speed: 500000 Hz
- LoRa GPIO control lines: Reset=26, IRQ/DIO1=23, Busy=22
- LoRa DIO2 RF switch and DIO3 TCXO voltage control are required
- Cardputer Zero user-session notifications are provided through the standard
`org.freedesktop.Notifications` D-Bus interface, not through a private app API
- Cardputer Zero user-session input method display is provided by a Fcitx5 UI
addon plus Wayland panel; text submission stays on the normal Linux input
method path
- display/input ownership for the real Pi OS path is not yet closed
- current dedicated app shell baseline is build-owned by
`apps/linux_cardputer_zero`
@@ -36,6 +41,7 @@ This record describes current repo evidence for the Linux Cardputer Zero route.
- evdev keyboard mapping sampled from the real device
- Trail Mate Linux SX1262 runtime validation against `/dev/spidev0.1` and the
documented GPIO lines
- notification daemon and Fcitx5 user-session integration on real device image
- launch/package route for the portable Linux device
The Linux simulator remains a separate target under `apps/linux_sim_shell`.
+2 -2
View File
@@ -19,7 +19,7 @@ routes.
| `twatch` | `twatch` | `builds/esp_idf` | `apps/esp32_lvgl` | ESP-IDF | LVGL | `watch_compact` | `compatibility` | `watch_compact_ui` | `watch_compact_manifest` | `watch_compact` | `PendingHardwareValidation` | current repo evidence is board and variant data, not migrated IDF defaults | `apps/esp32_lvgl + builds/esp_idf + boards/twatch` |
| `uconsole` | `uconsole` | `builds/linux_cmake` | `apps/linux_uconsole_gtk` | Linux | GTK | `uconsole_desktop` | `uconsole_desktop` | `uconsole_desktop_ui` | `uconsole_desktop_manifest` | `uconsole_desktop` | `Active` | none for UX pack selection | `apps/linux_uconsole_gtk + builds/linux_cmake + boards/uconsole` |
| `linux_sim` | `linux_sim` | `builds/linux_cmake` | `apps/linux_sim_shell` | Linux | ASCII | `simulator_full` | `simulator_full` | `simulator_full_ui` | `simulator_full_manifest` | `simulator_full` | `Active` | none for simulator UX pack selection | `apps/linux_sim_shell + builds/linux_cmake` |
| `cardputerzero` | `cardputerzero` | `builds/linux_cmake` | `apps/linux_cardputer_zero` | Linux | ASCII | `cardputer_compact` | `cardputer_compact` | `cardputer_compact_ui` | `cardputer_compact_manifest` | `cardputer_compact` | `PendingHardwareValidation` | framebuffer, evdev, launch, and packaging still need real device validation | `apps/linux_cardputer_zero + builds/linux_cmake + boards/cardputerzero` |
| `cardputerzero` | `cardputerzero` | `builds/linux_cmake` | `apps/linux_cardputer_zero` | Linux | ASCII | `cardputer_compact` | `cardputer_compact` | `cardputer_compact_ui` | `cardputer_compact_manifest` | `cardputer_compact` | `PendingHardwareValidation` | framebuffer, evdev, notifyd session, Fcitx5 session, launch, and packaging still need real device validation | `apps/linux_cardputer_zero + builds/linux_cmake + boards/cardputerzero` |
| `gat562_mesh_evb_pro` | `gat562_mesh_evb_pro` | `builds/pio_nrf52` | `apps/nrf52_node` | PlatformIO | Headless | `node_headless` | `tiny_node_status` | `node_headless_ui` | `node_headless_manifest` | `headless_node` | `Headless` | current executable UX pack is `tiny_node_status`; board hardware still records a 128 x 64 display | `apps/nrf52_node + builds/pio_nrf52 + boards/gat562_mesh_evb_pro` |
## Pending Evidence
@@ -29,7 +29,7 @@ routes.
| `tlora_pager` | ESP-IDF target defaults and hardware validation for the requested IDF route | `builds/esp_idf + boards/tlora_pager` |
| `tdeck` | ESP-IDF target defaults and hardware validation for the requested IDF route | `builds/esp_idf + boards/tdeck` |
| `twatch` | ESP-IDF target defaults and hardware validation for the requested IDF route | `builds/esp_idf + boards/twatch` |
| `cardputerzero` | real Cardputer Zero framebuffer, evdev, launch, and packaging validation | `apps/linux_cardputer_zero + boards/cardputerzero` |
| `cardputerzero` | real Cardputer Zero framebuffer, evdev, notifyd session, Fcitx5 session, launch, and packaging validation | `apps/linux_cardputer_zero + boards/cardputerzero` |
## Batch 2 Status Tokens
+2 -1
View File
@@ -35,7 +35,8 @@ absorb GTK, simulator, framebuffer, device-shell, or runtime behavior into
`cardputerzero` is no longer described as a future Raspberry Pi simulator path.
Its current owner is `apps/linux_cardputer_zero`; the remaining work is real
Cardputer Zero framebuffer, evdev, launch, and package validation.
Cardputer Zero framebuffer, evdev, notification daemon session, Fcitx5 session,
launch, and package validation.
## Non-Goals
+7 -2
View File
@@ -9,6 +9,11 @@ Compact 320 x 170 landscape keyboard device.
Keyboard-first input. Pointer, touch, and trackball are absent in the current
Cardputer Zero board facts.
On the Linux device route, text composition is owned by the user-session Fcitx5
stack. Trail Mate's compact UX accepts committed text from the normal Linux
input frontend path; the Cardputer Zero IME panel socket is display-only and is
not a Trail Mate text submission API.
## Feature Set
Compact chat/status workflow with compact map and practical GPS/team actions.
@@ -44,7 +49,7 @@ uses ASCII renderer until framebuffer ownership is validated.
## Deferred Decisions
Real-device keyboard sampling, framebuffer handoff, launch, and packaging remain
deferred.
Real-device keyboard sampling, framebuffer handoff, notification daemon session
validation, Fcitx5 session validation, launch, and packaging remain deferred.
Board describes. Target chooses. UX Pack presents. Renderer draws.