This chapter uses the managed FreeRTOS interface for instrument media ads. The SDK handles authenticated reception; you provide storage, durable receipts, decoding and playback.
Reception and deduplication
Each device deduplicates advertisement IDs in its own durable storage and starts playback at most once. The APP records reception only after the instrument receives, verifies and durably records the media. Changed media requires a new advertisement ID.
Reception and playback are separate. Power loss, leaving the authenticated session, playback failure or interruption never causes retransmission or playback on the next boot. Playback is allowed only during the current safe, idle session. Booting the instrument does not trigger advertisements.
Supported instrument media
| Purpose | Input constraints |
|---|---|
| Instrument still image | ≤ 5 MiB; dimensions must fit the instrument’s declared limits |
| Instrument GIF | ≤ 10 MiB, ≤ 10 seconds, ≤ 300 frames, ≤ 100 million cumulative decoded pixels, ≤ 1920×1080 |
| Instrument MP4 | H.264, ≤ 50 MiB, ≤ 15 seconds, ≤ 30fps, ≤ 1920×1080; transcoding removes audio |
Declare smaller capacity and dimension limits as needed, and only formats with implemented decoding and playback.
Terminal integration
Call moto_terminal_config_init, then set config.advertising using moto_ad_port from moto/advertising.h.
| Configuration | Requirement |
|---|---|
max_size / max_chunk |
Media/block byte limits; protocol maximum 50 MiB / 1024 bytes |
mime_mask |
Declare implemented formats with MOTO_AD_MIME_MASK(...) |
max_width / max_height |
Maximum decoded source dimensions before display scaling |
| Eight callbacks | Implement all; see signatures below and per-callback returns under “Media structure and return values” |
Callbacks run on the SDK task. They must be bounded, nonblocking and must not reenter the SDK. Compute hashes incrementally as blocks arrive. finish succeeds only after verification and durable receipt storage. Media may reside in RAM or Flash, but receipts must survive reboots, payload eviction and unpairing. If receipt storage is full, fail closed instead of deleting history and accepting old ads again.
Advertising requires a complete media port and cloud device authentication.
Wi-Fi control and data
Control uses the authenticated Wi-Fi business session. Optional bulk TLS carries media, with the primary session available for transfer from the confirmed offset. data.wifi.purpose=2 identifies advertising and data.wifi.task contains its UUID; OTA uses purpose=1.
Register TCP I/O through config.bulk before enabling wifi_advertising. For purpose 2, reuse the primary AP, verify IP/DHCP and prepare a task TCP listener, then call moto_terminal_bulk_ready. The SDK generates a fresh PSK and manages TLS; the same API supports OTA purpose 1. The bulk connection may write only the currently authorized advertisement; status, finish and playback remain on the Wi-Fi business session. Release task TCP and playback resources on disconnect, stop or session replacement; retain the primary hotspot and discovery listener.
Advertisement transfer and playback are mutually exclusive with navigation, video, instrument-style changes and OTA. Use vehicle and display state in safe_idle to allow playback without interrupting navigation.
Configuration example and callback signatures
Configure media with moto_terminal_config.advertising. The separate moto_terminal_advertising(t, out) API generates BLE discovery data.
This example enables PNG only. Implement board_ad_* in your board drivers and set capacity and dimensions to match the hardware. Call configure_advertising after moto_terminal_config_init and before moto_terminal_create.
#include <moto_freertos/terminal.h>
#include <moto/advertising.h>
int board_ad_lookup(void *u, const uint8_t ad[16],
uint8_t hash[32], bool *consumed);
int board_ad_begin(void *u, const moto_ad_media *media);
int board_ad_write(void *u, uint32_t offset,
const uint8_t *bytes, size_t size);
int board_ad_finish(void *u, const moto_ad_media *media);
int board_ad_consume(void *u, const uint8_t ad[16]);
int board_ad_play(void *u, const moto_ad_media *media);
void board_ad_stop(void *u);
bool board_ad_safe_idle(void *u);
void configure_advertising(moto_terminal_config *c, void *board_context) {
c->advertising = (moto_ad_port){
.user = board_context,
.max_size = 256u * 1024u,
.max_chunk = 512,
.mime_mask = MOTO_AD_MIME_MASK(MOTO_AD_PNG),
.max_width = 800,
.max_height = 480,
.lookup = board_ad_lookup,
.begin = board_ad_begin,
.write = board_ad_write,
.finish = board_ad_finish,
.consume = board_ad_consume,
.play = board_ad_play,
.stop = board_ad_stop,
.safe_idle = board_ad_safe_idle,
};
c->wifi_advertising = false;
}
An all-zero media port disables advertising. Partial callbacks or invalid limits make create return INVALID at the CONFIG stage. Use moto_ad_port_validate to check the port separately.
advertising.user is independent of port.user; only its pointer is copied at creation, and it must remain valid until SDK destruction. Inputs such as media, ad and bytes are borrowed for the callback only. Copy metadata or bytes for asynchronous display instead of retaining borrowed addresses.
Media structure and return values
moto_ad_media describes media that has passed protocol validation:
| Field | Type and meaning |
|---|---|
ad[16] |
The 16 raw UUID bytes used for durable deduplication; not a NUL-terminated string |
task[16] |
Raw UUID bytes identifying the transfer task; never substitute this for the advertisement ID |
sha256[32] |
SHA-256 of the complete media file; the driver must actually verify it |
size |
uint32_t, total file bytes from 1 through the port’s max_size |
mime |
uint8_t: 1 PNG, 2 JPEG, 3 GIF, 4 H.264 MP4; the mask uses 1u << mime |
width / height |
uint16_t, original media dimensions within declared decoder limits |
duration_ms |
uint32_t, at most 15000 milliseconds; 0 selects a 3000-millisecond SDK stop timer; the driver may end earlier when media ends naturally |
The port’s max_size is 1–52428800 bytes and max_chunk is 1–1024 bytes. Dimension limits must be nonzero. Protocol ceilings do not establish board memory, storage or decoder performance.
| Callback | Successful return | Failure handling |
|---|---|---|
lookup |
0 absent; 1 received, filling hash and consumed |
Return a negative error on storage failure instead of treating the record as absent |
begin / write / finish / play |
MOTO_OK (0), after that step has actually completed |
Return a negative moto_result, such as MOTO_IO, MOTO_FULL or MOTO_UNSUPPORTED |
consume |
0 newly consumed; 1 already consumed | Return a negative error if durable storage fails |
stop |
No return value; safe to repeat, including without active media | Release reception/playback and their dedicated resources while preserving durable receipts |
safe_idle |
true permits presentation; false refuses a start or stops active playback |
Use actual vehicle and display state |
A return of 1 from lookup/consume is a business result, not an asynchronous wait. Advertising callbacks cannot return MOTO_AGAIN and later complete through moto_terminal_complete. There is no MOTO_EVENT_AD_* delivering media through the application event loop. Callbacks execute synchronously on the SDK task and must be bounded; do not wait for long Flash work on another task or for the full presentation to end. Hash incrementally in write, then verify and atomically commit the receipt in finish. Evaluate whether this synchronous contract meets the target storage latency before enabling it.
Reception and playback sequence
lookup → begin → write → finish → consume → play → stop
Reception and actual exposure are separate. After finish, lookup must immediately return the same persisted hash. consume commits before play. A playback failure or power loss between those steps must not clear consumption or schedule playback on a later connection or boot.
write receives the contiguous byte offset. Preserve receipts after finish; queries for received media return the stored record without replaying it.
Advertising data-channel APIs
Register config.bulk.send/close and enable wifi_advertising. The board supplies the hotspot, IP/DHCP, TCP listener and ordered byte I/O; the SDK handles TLS authentication and data reception.
| API | Usage and result |
|---|---|
moto_terminal_bulk_ready(t, &e, &ap) |
Submit the saved purpose-1/2 WIFI event after AP/IP/TCP listener readiness; ap.port is the TCP port. OK means queued; FULL means not submitted, retry original arguments |
moto_terminal_bulk_connected(t, &e, &connection) |
On TCP accept, pass the WIFI event saved with that listener. OK returns a new nonzero generation; BUSY means an existing connection; STATE means readiness has not been processed or the event is stale, so close this socket |
moto_terminal_bulk_receive(t, connection, bytes, size) |
1–512 TLS bytes per call; OK copies all, FULL accepts none. Retain and retry in order without skipping or duplicating |
moto_terminal_bulk_disconnected(t, connection) |
Revoke the TCP generation directly, even with a full receive queue; stale generations return STATE |
config.bulk.send(user, connection, bytes, size) |
Called on the SDK task; copy accepted bytes and return their count, 0 for backpressure, negative for failure; never block |
config.bulk.close(user, connection) |
Asynchronously and idempotently close exactly this TCP generation during SDK cleanup; do not close a later connection |
moto_terminal_complete(t, &e, MOTO_FAILED, reason) |
Report hotspot preparation failure; do not acknowledge media writes or playback through this API |
Keep bulk.user alive until destruction. Callback byte pointers are borrowed only until return. bulk_rx_slots defaults to 8, range 1–128, and is allocated only for managed bulk. All entry points are task APIs, never ISR APIs. Copy the original WIFI event into listener context; do not substitute the latest session.
The SDK accepts one bulk TCP connection at a time. Clean up on disconnect, WIFI_STOP or session expiry; a new transfer requires a new WIFI event and readiness flow. Inspect error events and get_status().error for asynchronous failures. Stop BLE/TCP input and event consumers before terminal destruction.
Before switching from bulk to the primary session, close bulk and query the device’s confirmed receive offset. Do not write through both transports concurrently.
