Skip to content
This repository was archived by the owner on May 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions examples/EmergencyManagement/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,31 @@ Both the CLI demo and the FastAPI gateway read [`client/client_config.json`](cli
"request_timeout_seconds": 30,
"lxmf_config_path": null,
"lxmf_storage_path": null,
"shared_instance_rpc_key": "<hex rpc key>"
"shared_instance_rpc_key": "<hex rpc key>",
"generate_test_messages": false,
"enable_interactive_menu": true,
"test_message_count": 5,
"test_event_count": 5
}
```

- Override the location of the configuration file with `NORTH_API_CONFIG_PATH` or provide JSON directly through `NORTH_API_CONFIG_JSON`.
- Requests can target different LXMF services by supplying an `X-Server-Identity` header or a `server_identity` query parameter to the gateway.
- The repository ships with a sample Reticulum directory at [`examples/EmergencyManagement/.reticulum`](./.reticulum) that pins `rpc_key` to `F1E2D3C4B5A697887766554433221100`. When the gateway and LXMF service use this directory (or any config with the same key) they can attach to the same shared instance without prompting.

| Key | Purpose |
| --- | --- |
| `server_identity_hash` | Destination LXMF identity hash for the Emergency service. |
| `client_display_name` | Friendly name announced by the LXMF client. |
| `request_timeout_seconds` | Timeout applied to each LXMF command issued by the client or gateway. |
| `lxmf_config_path` | Optional override for the Reticulum configuration directory. |
| `lxmf_storage_path` | Optional override for the LXMF storage directory. |
| `shared_instance_rpc_key` | RPC key used when attaching to a shared Reticulum instance. |
| `generate_test_messages` | When `true`, the CLI seeds random emergency messages and events during startup. |
| `enable_interactive_menu` | Enables the interactive CLI menu after the initial demo run. Disable when scripting automated flows. |
| `test_message_count` | Number of emergency messages to seed when `generate_test_messages` is enabled. |
| `test_event_count` | Number of events to seed when `generate_test_messages` is enabled. |

### Web UI environment

Copy [`webui/.env.example`](webui/.env.example) to `webui/.env` and set:
Expand Down Expand Up @@ -90,7 +107,7 @@ Copy [`webui/.env.example`](webui/.env.example) to `webui/.env` and set:
python client/client_emergency.py
```

The client reuses the stored identity hash (or prompts for one), sends a `CreateEmergencyActionMessage` command, and then retrieves the stored record to verify persistence in `emergency.db`.
The client reuses the stored identity hash (or prompts for one) and exposes an interactive menu for creating, listing, updating, retrieving, and deleting emergency action messages over LXMF.

3. **Expose the REST gateway**

Expand Down
15 changes: 14 additions & 1 deletion examples/EmergencyManagement/client/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# Emergency Management northbound client

The full stack setup—including the shared LXMF client, FastAPI gateway, and CLI demo—is documented in the consolidated [Emergency Management README](../README.md). Refer to that guide for configuration values, startup commands, and build instructions. The sample [`client_config.json`](client_config.json) now also includes a `shared_instance_rpc_key` entry so the CLI, web gateway, and LXMF service can all authenticate with the bundled Reticulum configuration under [`../.reticulum`](../.reticulum).
The full stack setup—including the shared LXMF client, FastAPI gateway, and CLI demo—is documented in the consolidated [Emergency Management README](../README.md). Refer to that guide for configuration values, startup commands, and build instructions. The CLI now presents an interactive menu that lets operators:

- create new emergency action messages,
- update existing records,
- list all stored messages,
- retrieve individual messages by callsign, and
- delete records when they are no longer needed.

All operations use the shared LXMF client and message codecs, so the CLI mirrors the behaviour of the gateway and northbound API. Populate [`client_config.json`](client_config.json) with values that match your mesh. In addition to the LXMF configuration paths and RPC key, the client understands:

- `request_timeout_seconds` – per-command timeout budget,
- `generate_test_messages` – optional seeding of random demo data,
- `enable_interactive_menu` – turn the interactive prompt on or off (useful for automation),
- `test_message_count` / `test_event_count` – payload counts for the seeding routine.
131 changes: 128 additions & 3 deletions examples/EmergencyManagement/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from examples.EmergencyManagement.Server.models_emergency import (
EmergencyActionMessage,
Event,
DeleteEmergencyActionMessageResult,
)

_JSON_DECODE_FAILED = object()
Expand Down Expand Up @@ -41,6 +42,9 @@ def _decode_json_payload(payload: Optional[bytes], target_type):


COMMAND_CREATE_EMERGENCY_ACTION_MESSAGE = "CreateEmergencyActionMessage"
COMMAND_DELETE_EMERGENCY_ACTION_MESSAGE = "DeleteEmergencyActionMessage"
COMMAND_LIST_EMERGENCY_ACTION_MESSAGE = "ListEmergencyActionMessage"
COMMAND_PUT_EMERGENCY_ACTION_MESSAGE = "PutEmergencyActionMessage"
COMMAND_RETRIEVE_EMERGENCY_ACTION_MESSAGE = "RetrieveEmergencyActionMessage"
COMMAND_CREATE_EVENT = "CreateEvent"
COMMAND_DELETE_EVENT = "DeleteEvent"
Expand Down Expand Up @@ -69,12 +73,86 @@ def _decode_emergency_action_message(
if payload is None:
raise ValueError("Response payload is required")

json_result = _decode_json_payload(payload, EmergencyActionMessage)
if json_result is not _JSON_DECODE_FAILED:
if json_result is None:
raise ValueError("Decoded payload cannot be null")
return json_result

data = from_bytes(payload)
if not isinstance(data, dict):
raise ValueError("Decoded payload must be a mapping")
return EmergencyActionMessage(**data)


def _decode_optional_emergency_action_message(
payload: Optional[bytes],
) -> Optional[EmergencyActionMessage]:
"""Return an optional :class:`EmergencyActionMessage` decoded from bytes."""

if payload is None:
return None

json_result = _decode_json_payload(payload, EmergencyActionMessage)
if json_result is not _JSON_DECODE_FAILED:
return json_result

data = from_bytes(payload)
if data is None:
return None
if not isinstance(data, dict):
raise ValueError("Decoded payload must be a mapping")
return EmergencyActionMessage(**data)


def _decode_emergency_action_message_list(
payload: Optional[bytes],
) -> List[EmergencyActionMessage]:
"""Return a list of :class:`EmergencyActionMessage` decoded from bytes."""

if payload is None:
return []

json_result = _decode_json_payload(payload, List[EmergencyActionMessage])
if json_result is not _JSON_DECODE_FAILED:
if json_result is None:
return []
return list(json_result)

data = from_bytes(payload)
if data is None:
return []
if not isinstance(data, list):
raise ValueError("Decoded payload must be a list")

messages: List[EmergencyActionMessage] = []
for item in data:
if not isinstance(item, dict):
raise ValueError("Each emergency action payload must be a mapping")
messages.append(EmergencyActionMessage(**item))
return messages


def _decode_delete_emergency_action_message_result(
payload: Optional[bytes],
) -> DeleteEmergencyActionMessageResult:
"""Return the delete emergency action response decoded from bytes."""

if payload is None:
raise ValueError("Response payload is required")

json_result = _decode_json_payload(payload, DeleteEmergencyActionMessageResult)
if json_result is not _JSON_DECODE_FAILED:
if json_result is None:
raise ValueError("Decoded payload cannot be null")
return json_result

data = from_bytes(payload)
if not isinstance(data, dict):
raise ValueError("Decoded payload must be a mapping")
return DeleteEmergencyActionMessageResult(**data)


def _decode_event(payload: Optional[bytes]) -> Event:
"""Return an :class:`Event` decoded from MessagePack bytes."""

Expand Down Expand Up @@ -183,7 +261,7 @@ async def retrieve_emergency_action_message(
client: LXMFClient,
server_identity_hash: str,
callsign: str,
) -> EmergencyActionMessage:
) -> Optional[EmergencyActionMessage]:
"""Fetch an emergency action message from the LXMF API.

Args:
Expand All @@ -192,7 +270,7 @@ async def retrieve_emergency_action_message(
callsign (str): Callsign identifying the message to retrieve.

Returns:
EmergencyActionMessage: Retrieved message returned by the service.
Optional[EmergencyActionMessage]: Retrieved message or ``None`` when missing.
"""

response = await client.send_command(
Expand All @@ -201,7 +279,54 @@ async def retrieve_emergency_action_message(
callsign,
await_response=True,
)
return _decode_emergency_action_message(response)
return _decode_optional_emergency_action_message(response)


async def list_emergency_action_messages(
client: LXMFClient,
server_identity_hash: str,
) -> List[EmergencyActionMessage]:
"""Return all emergency action messages stored on the LXMF service."""

response = await client.send_command(
server_identity_hash,
COMMAND_LIST_EMERGENCY_ACTION_MESSAGE,
None,
await_response=True,
)
return _decode_emergency_action_message_list(response)


async def update_emergency_action_message(
client: LXMFClient,
server_identity_hash: str,
message: EmergencyActionMessage,
) -> Optional[EmergencyActionMessage]:
"""Update an emergency action message via the LXMF API."""

response = await client.send_command(
server_identity_hash,
COMMAND_PUT_EMERGENCY_ACTION_MESSAGE,
message,
await_response=True,
)
return _decode_optional_emergency_action_message(response)


async def delete_emergency_action_message(
client: LXMFClient,
server_identity_hash: str,
callsign: str,
) -> DeleteEmergencyActionMessageResult:
"""Delete an emergency action message via the LXMF API."""

response = await client.send_command(
server_identity_hash,
COMMAND_DELETE_EMERGENCY_ACTION_MESSAGE,
callsign,
await_response=True,
)
return _decode_delete_emergency_action_message_result(response)


async def create_event(
Expand Down
5 changes: 4 additions & 1 deletion examples/EmergencyManagement/client/client_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"lxmf_config_path": null,
"lxmf_storage_path": null,
"generate_test_messages": true,
"shared_instance_rpc_key": "F1E2D3C4B5A697887766554433221100"
"enable_interactive_menu": true,
"shared_instance_rpc_key": "F1E2D3C4B5A697887766554433221100",
"test_message_count": 5,
"test_event_count": 5
}
Loading
Loading