Sensors - Integrating Hardware Sensorsο
In this tutorial, we implement a sensors dashboard app that subscribes to available sensors at maximum frequency (period=0, count=0 except Accelerometer), processes new message structures with timestamps where available, and displays data on a single GUI screen:
Top: Battery level
Middle multiline text: Sensor data with L1/L2 verbosity levels (BASIC/DETAILED/FULL and individual sensor views)
Bottom: Stats (Service/GUI CPU%, TX/RX msg rates, bytes/sec)
List of Implemented Sensorsο
Sensor |
Description |
Notes |
|---|---|---|
Heart Rate |
Live BPM + Trust Level |
|
GPS Location |
Lat/Long/Alt (double) |
Always on |
Altimeter |
Elevation (m) |
Barometric pressure based |
Accelerometer |
X/Y/Z G-forces |
connect(0.1f, 0); sender throttled ~100ms |
Step Counter |
Total steps |
Cumulative |
Floor Counter |
Floors ascended |
Cumulative; parser.getFloorsUp() |
Magnetometer |
X/Y/Z fields (for compass) |
Heading computed from X/Y fields |
RTC |
Time (sec since boot) |
From kernel sys.getTimeMs()/1000; not sensor |
Architecture Overviewο
graph LR
Sensors[(Sensors HW)] --> SDL[Sensor Data Layer]
SDL -->|EVENT_SENSOR_LAYER_DATA| Service[Service Thread]
Service -->|Custom Msgs w/ timestamps| Kernel[(Kernel)]
Kernel -->|Custom Msgs| Model[GUI Model]
Model --> View[MainView]
View --> Display["Single Screen:
Battery Top
Multiline Data (L1/L2)
Bottom Stats"]
Buttons[L1/L2 Buttons] -.-> View
Implementation Stepsο
Optional step: copy the Sensor tutorial code to editο
Copy sensors tutorial
Change naming: Rename project directory, cmake directory and name of the project in CMakeLists.txt; Also change APP_ID to something else, Step 2 in Creating New Apps gives commmands for generating your own app ID programatically from the name.
Commit initial changes: itβs a good practice to use version control system like git
*Edit TouchGFX if you changed the name:
Rename
*.touchgfxto<MY_APP>.touchgfxRename
*.touchgfx:163"Name": "MY_APP"Click Generate code
Step 1: Define Custom Messagesο
Create Commands.hpp with message types and structs:
namespace CustomMessage {
constexpr SDK::MessageType::Type HR_VALUES = 0x00000001;
constexpr SDK::MessageType::Type LOCATION_VALUES = 0x00000002;
constexpr SDK::MessageType::Type ELEVATION_VALUES = 0x00000003;
constexpr SDK::MessageType::Type ACCELEROMETER_VALUES = 0x00000004;
constexpr SDK::MessageType::Type STEP_COUNTER_VALUES = 0x00000005;
constexpr SDK::MessageType::Type FLOORS_VALUES = 0x00000006;
constexpr SDK::MessageType::Type COMPASS_VALUES = 0x00000007;
constexpr SDK::MessageType::Type STATS_VALUES = 0x00000008;
constexpr SDK::MessageType::Type RTC_VALUES = 0x00000009;
constexpr SDK::MessageType::Type BATTERY_VALUES = 0x0000000A;
constexpr SDK::MessageType::Type PRESSURE_VALUES = 0x0000000B;
// Every message pairs a default constructor, which sets the message type,
// with one that fills the fields and delegates to it -- the shape every
// example app uses, which keeps the type tag in a single initializer. That
// second constructor is what lets a caller send the message in a single call.
struct HRValues : public SDK::MessageBase {
float heartRate;
float trustLevel;
HRValues()
: SDK::MessageBase(HR_VALUES)
, heartRate()
, trustLevel()
{}
explicit HRValues(float heartRate, float trustLevel)
: HRValues()
{
this->heartRate = heartRate;
this->trustLevel = trustLevel;
}
};
// The rest follow the same shape, over these fields:
// LocationValues: uint64_t timestamp; double latitude, longitude, altitude;
// ElevationValues: uint64_t timestamp; float elevation;
// AccelerometerValues: uint64_t timestamp; float x,y,z;
// StepCounterValues: uint64_t timestamp; uint32_t steps;
// FloorsValues: uint64_t timestamp; uint32_t floors;
// CompassValues: uint64_t timestamp; float heading;
// StatsValues: float serviceCpuPct, guiCpuPct, txMsgRate, rxMsgRate, txByteRate, rxByteRate;
// RtcValues: uint32_t time;
// BatteryValues: float level;
// PressureValues: uint64_t timestamp; float pressure;
}
PressureValues is the one message the service never sends: the sensor is connected and Model.cpp
handles PRESSURE_VALUES, but the service only hex-dumps the raw frame today. Both ends exist, and
so does SDK::SensorDataParser::Pressure β wiring it up means giving that branch the same
parse-then-send shape as the branches below, using parser.getPressure().
Those constructors are what let SDK::send_msg<T>(kernel, args...) do the whole send in one call β
allocate from the kernel pool, forward the arguments to the constructor, send, and release. The app
needs no sender class of its own.
send_msg is for fire-and-forget sends, and it posts with a zero timeout β it never waits for a
reply, and a message that finds no room in the queue is dropped. That is the right trade for sensor
data: the next sample is moments away. Reach for SDK::make_msg<T>() when the reply matters: it
returns an RAII MessageGuard that releases on scope exit, so you can send with a timeout and read
the result back (msg.send(timeout) && msg.ok()). That timeout bounds the wait for the reply, not for
queue space β no send waits out a full queue.
Step 2: Service - Subscribe & Process Sensorsο
In Service.hpp:
SDK::Sensor::Connection mSensorHR{SDK::Sensor::Type::HEART_RATE, 0, 0};
SDK::Sensor::Connection mSensorGPS{SDK::Sensor::Type::GPS_LOCATION, 0, 0};
SDK::Sensor::Connection mSensorAltimeter{SDK::Sensor::Type::ALTIMETER, 0, 0};
SDK::Sensor::Connection mSensorAccelerometer{SDK::Sensor::Type::ACCELEROMETER, 0, 0};
SDK::Sensor::Connection mSensorStepCounter{SDK::Sensor::Type::STEP_COUNTER, 0, 0};
SDK::Sensor::Connection mSensorFloorCounter{SDK::Sensor::Type::FLOOR_COUNTER, 0, 0};
In run() Service.cpp: connect all (acc.connect(0.1f, 0)), loop getMessage:
case SDK::MessageType::EVENT_SENSOR_LAYER_DATA: {
auto event = static_cast<SDK::Message::Sensor::EventData*>(msg);
SDK::Sensor::DataBatch data(event->data, event->count, event->stride);
onSdlNewData(event->handle, data);
} break;
In onSdlNewData:
if (mSensorHR.matchesDriver(handle)) {
SDK::SensorDataParser::HeartRate parser(data[0]);
if (parser.isDataValid()) {
SDK::send_msg<CustomMessage::HRValues>(mKernel, parser.getBpm(), parser.getTrustLevel());
}
}
// Similar for GPS: SDK::send_msg<CustomMessage::LocationValues>(mKernel, ts, lat, lon, alt);
// Altimeter: SDK::send_msg<CustomMessage::ElevationValues>(mKernel, ts, parser.getAltitude());
// Accel: if (nowMs - mLastAccTimeMs >= 100) SDK::send_msg<CustomMessage::AccelerometerValues>(mKernel, ts, x, y, z);
// Steps: SDK::send_msg<CustomMessage::StepCounterValues>(mKernel, ts, parser.getStepCount());
// Floors: SDK::send_msg<CustomMessage::FloorsValues>(mKernel, ts, parser.getFloorsUp());
// Compass: compute heading from magnetic X/Y fields, then SDK::send_msg<CustomMessage::CompassValues>(mKernel, ts, heading);
Track stats every 1s (simplistic CPU% = ms/10, rates=counts/sec) with
SDK::send_msg<CustomMessage::StatsValues>(...), and
SDK::send_msg<CustomMessage::RtcValues>(mKernel, timeMs/1000).
Step 3: GUI Model - Receive Messagesο
In Model.cpp, implement customMessageHandler:
case CustomMessage::HR_VALUES: {
auto* m = static_cast<CustomMessage::HRValues*>(msg);
modelListener->updateHR(m->heartRate, m->trustLevel);
} break;
// Similar cases for all types: LOCATION_VALUES -> updateGPS(m->latitude, m->longitude, m->altitude);
// etc. for Elevation, Accelerometer, StepCounter, Floors, Compass, Stats, RTC
Step 4: MainView - Display & Controlsο
In MainView.hpp, MainView.cpp:
Store data in members, updateHR(float hr, float tl) etc. store values.
handleKeyEvent: L1: verbosity++ % VERB_LEVEL_MAX, L2: verbosityβ % VERB_LEVEL_MAX (BASIC/DETAILED/FULL/HR/GPS/ALT/ACC/STEP/FLOOR/MAG), R1: TODO GPS toggle, R2: presenter->exit()
handleTickEvent() every tick: refreshDisplay() refreshStats() refreshBattery()
refreshDisplay(): format multiline in text_body based on verbosity level, with group display for BASIC/DETAILED/FULL and per-sensor detailed views for individual sensors.
Unicode::strncpy(text_bodyBuffer, buffer, TEXT_BODY_SIZE); invalidate
Header: refreshBattery() βBattery: %.1f%%β text_header
Stats: refreshStats() βCPU S: %.1f%% G: %.1f%%\nMsg Tx: %.0f Rx: %.0f\nBytes Tx: %.0f Rx: %.0fβ text_stats
Additional Notesο
Battery: Battery level retrieval implemented using SDK::Sensor::BATTERY_LEVEL
Altimeter: Uses altitude from barometric sensor; pressure not available in parser
Max Frequency: period=0,count=0 except Accel connect(0.1f, 0); sender Accel throttle 100ms.
RTC: Kernel sys.getTimeMs()/1000 (seconds since boot), not SDK::Sensor::RTC.
Build with
CMakeLists.txt.
Running on Simulatorο
To test the Sensors app on the simulator (Windows only):
Build the app following the SDK setup instructions.
Open
Sensors.touchgfxin TouchGFX Designer and click Generate Code (F4) (do this once).Navigate to
Sensors\Software\Apps\TouchGFX-GUI\simulator\msvsOpen
Application.vcxprojin Visual StudioPress F5 to start debugging and run the simulator
The simulator provides simulated sensor data for all implemented sensors. Use L1/L2 buttons to cycle through verbosity levels and view different sensor data displays.
For detailed sensor simulation configuration and features, see Simulator.