Plotting your own Data

Max Gallup
Hardware Lead, Project Administrator
The Web-UI features not only the live feed from the camera, but also has the option to plot scalar values. All of the VU-ASE services have built-in types that the WebUI Recognizes to be able to plot them. But what if you would like to plot your own values? For this, we have two "generic" scalar types that you can use to plot values. The full list of generic messages can be found here, however the only two that can be plotted are GenericFloatScalar
and GenericIntScalar
.
This is what a data message will look like when plotted, in this case we are just sending a dummy sine data. Notice there is a custom label we can add "some_string"
. This is only to help you organize which streams are being printed, we recommend to use unique and descriptive names.
The following are example services for each language that only plot sine data as shown in the picture above:
- Go
- Python
- C
- C++
- Other languages
For this to work, make sure you add the testing
output to your service.yaml
!
package main
import (
"fmt"
"os"
"time"
"math"
pb_outputs "github.com/VU-ASE/rovercom/packages/go/outputs"
roverlib "github.com/VU-ASE/roverlib-go/src"
"github.com/rs/zerolog/log"
)
// The main user space program
// this program has all you need from roverlib: service identity, reading, writing and configuration
func run(service roverlib.Service, configuration *roverlib.ServiceConfiguration) error {
//
// Writing to an output that other services can read (see service.yaml to understand the output name)
//
writeStream := service.GetWriteStream("testing")
if writeStream == nil {
return fmt.Errorf("Failed to create write stream 'testing'")
}
angle := 0.0
for {
sineValue := math.Sin(angle)
// Initialize the message that we want to send to the actuator
output_msg := pb_outputs.SensorOutput{
Timestamp: uint64(time.Now().UnixMilli()), // milliseconds since epoch
Status: 0, // all is well
SensorId: 1,
SensorOutput: &pb_outputs.SensorOutput_GenericFloatScalar{
GenericFloatScalar: &pb_outputs.GenericFloatScalar{
Key: "some_string",
Value: float32(sineValue),
},
},
}
// Send the message to the actuator
err := writeStream.Write(&output_msg)
if err != nil {
log.Warn().Err(err).Msg("Could not write to actuator")
}
angle += 0.1
time.Sleep(100 * time.Millisecond)
}
}
// This function gets called when roverd wants to terminate the service
func onTerminate(sig os.Signal) error {
log.Info().Str("signal", sig.String()).Msg("Terminating service")
return nil
}
// This is just a wrapper to run the user program
// it is not recommended to put any other logic here
func main() {
roverlib.Run(run, onTerminate)
}
For this to work, make sure you add the testing
output to your service.yaml
!
#!/usr/bin/python3
import time
import math
import signal
from loguru import logger
import roverlib
import roverlib.rovercom as rovercom
# The main user space program
# this program has all you need from roverlib: service identity, reading, writing and configuration
def run(service : roverlib.Service, configuration : roverlib.ServiceConfiguration):
#
# Writing to an output that other services can read (see service.yaml to understand the output name)
#
write_stream : roverlib.WriteStream = service.GetWriteStream("testing")
if write_stream is None:
raise ValueError("Failed to create write stream 'testing'")
angle = 0
while True:
sine_value = math.sin(angle)
write_stream.Write(rovercom.SensorOutput(
sensor_id=1,
timestamp=int(time.time() * 1000),
generic_float_scalar=rovercom.GenericFloatScalar(
key = "some_string",
value = sine_value
)
))
angle += 0.2 # Increment angle for next iteration
time.sleep(0.05) # Add small delay to control rate
# This function gets called when the pipeline gets terminated. This can happen if any other service
# in the pipeline crashes or if the pipeline is stopped via the the web interface.
def on_terminate(sig : signal):
logger.info(f"signal: {str(sig)}, Terminating service")
return None
if __name__ == "__main__":
roverlib.Run(run, on_terminate)
For this to work, make sure you add the testing
output to your service.yaml
! Additionally, make sure to add the -lm
flag to your compiler command to include the math library!
#include <roverlib.h>
#include <sys/time.h>
#include <unistd.h>
#include <math.h>
char* SOME_KEY = "some_string";
long long current_time_millis() {
struct timeval tv;
gettimeofday(&tv, NULL);
return (long long)(tv.tv_sec) * 1000 + (tv.tv_usec) / 1000;
}
float get_value(float *angle)
{
float value = sin(*angle);
// Sleep for 10ms
usleep(1000);
// Increment angle (small enough to see pattern changes)
*angle += 0.1f;
// Reset angle after a complete cycle to avoid floating-point issues
if (*angle >= 2.0f * M_PI)
{
*angle -= 2.0f * M_PI;
}
return value;
}
// The main user space program
// this program has all you need from roverlib: service identity, reading,
// writing and configuration
int user_program(Service service, Service_configuration *configuration) {
//
// Writing to an output that other services can read (see service.yaml to
// understand the output name)
//
write_stream *write_stream = get_write_stream(&service, "testing");
if (write_stream == NULL) {
printf("Failed to create write stream 'testing'\n");
}
float fun_value = 0.0;
while (true) {
// Initialize the message that we want to send to the actuator
ProtobufMsgs__SensorOutput output_msg = PROTOBUF_MSGS__SENSOR_OUTPUT__INIT;
// Set the message fields
output_msg.timestamp = current_time_millis(); // milliseconds since epoch
output_msg.status = 0; // all is well
output_msg.sensorid = 1;
ProtobufMsgs__GenericFloatScalar generic_output = PROTOBUF_MSGS__GENERIC_FLOAT_SCALAR__INIT;
generic_output.key = SOME_KEY;
generic_output.value = get_value(&fun_value);
output_msg.genericfloatscalar = &generic_output;
output_msg.sensor_output_case = PROTOBUF_MSGS__SENSOR_OUTPUT__SENSOR_OUTPUT_GENERIC_FLOAT_SCALAR;
// Send the message to the actuator
int res = write_pb(write_stream, &output_msg);
if (res <= 0)
{
printf("Could not write to actuator\n");
return 1;
}
}
}
// This is just a wrapper to run the user program
// it is not recommended to put any other logic here
int main() { return run(user_program); }
For this to work, make sure you add the testing
output to your service.yaml
!
// Need to use extern "C" to prevent C++ name mangling
extern "C"
{
#include "../lib/include/roverlib.h"
}
#include <sys/time.h>
#include <unistd.h>
#include <chrono>
#include <thread>
#include <math.h>
#include <iostream>
char* SOME_KEY = "some_string";
long long current_time_millis()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return (long long)(tv.tv_sec) * 1000 + (tv.tv_usec) / 1000;
}
float get_value(float *angle)
{
float value = sin(*angle);
// Sleep for 10ms
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Increment angle (small enough to see pattern changes)
*angle += 0.1f;
// Reset angle after a complete cycle to avoid floating-point issues
if (*angle >= 2.0f * M_PI)
{
*angle -= 2.0f * M_PI;
}
return value;
}
// The main user space program
// this program has all you need from roverlib: service identity, reading, writing and configuration
int user_program(Service service, Service_configuration *configuration)
{
// Setting the buffer of stdout to NULL in order to print the logs in the webUI
setbuf(stdout, NULL);
//
// Get configuration values
//
if (configuration == NULL)
{
printf("Configuration cannot be accessed\n");
return 1;
}
//
// Access the service identity, who am I?
//
printf("Hello world, a new C service '%s' was born at version %s\n", service.name, service.version);
//
// Writing to an output that other services can read (see service.yaml to understand the output name)
//
write_stream *write_stream = get_write_stream(&service, "testing");
if (write_stream == NULL)
{
printf("Failed to create write stream 'testing'\n");
}
float fun_value = 0.0;
while (true)
{
// Initialize the message that we want to send to the actuator
ProtobufMsgs__SensorOutput output_msg = PROTOBUF_MSGS__SENSOR_OUTPUT__INIT;
// Set the message fields
output_msg.timestamp = current_time_millis(); // milliseconds since epoch
output_msg.status = 0; // all is well
output_msg.sensorid = 1;
ProtobufMsgs__GenericFloatScalar generic_output = PROTOBUF_MSGS__GENERIC_FLOAT_SCALAR__INIT;
generic_output.key = SOME_KEY;
generic_output.value = get_value(&fun_value);
output_msg.genericfloatscalar = &generic_output;
output_msg.sensor_output_case = PROTOBUF_MSGS__SENSOR_OUTPUT__SENSOR_OUTPUT_GENERIC_FLOAT_SCALAR;
// Send the message to the actuator
int res = write_pb(write_stream, &output_msg);
if (res <= 0)
{
printf("Could not write to actuator\n");
return 1;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
// This is just a wrapper to run the user program
// it is not recommended to put any other logic here
int main()
{
return run(user_program);
}
For other languages, look up the examples of other languages to understand what protobuf definitions you need to send.