Skip to content

lupyuen/visual-zig-nuttx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Visual Programming with Zig and NuttX Sensors on Blockly

Visual Programming for Zig with NuttX Sensors

Read the articles...

Can we use Scratch / Blockly to code Zig programs, the drag-n-drop way?

Let's create a Visual Programming Tool for Zig that will generate IoT Sensor Apps with Apache NuttX RTOS.

Why limit to IoT Sensor Apps?

  • Types are simpler: Only floating-point numbers will be supported, no strings needed

  • Blockly is Typeless. With Zig we can use Type Inference to deduce the missing Struct Types

  • Make it easier to experiment with various IoT Sensors: Temperature, Humidity, Air Pressure, ...

Blockly Source Code: lupyuen3/blockly-zig-nuttx

Visual Programming for Zig with NuttX Sensors

Sensor Test App in C

We start with the Sensor Test App (in C) from Apache NuttX RTOS: sensortest.c

Here are the steps for reading a NuttX Sensor...

// From https://lupyuen.github.io/articles/bme280#sensor-test-app
// Open the Sensor Device.
// devname looks like "/dev/uorb/sensor_baro0" or "/dev/uorb/sensor_humi0"
fd = open(devname, O_RDONLY | O_NONBLOCK);

// Set Standby Interval
ioctl(fd, SNIOC_SET_INTERVAL, interval);

// Set Batch Latency
ioctl(fd, SNIOC_BATCH, latency);

//  If Sensor Data is available...
if (poll(&fds, 1, -1) > 0) {

  //  Read the Sensor Data
  if (read(fd, buffer, len) >= len) {

    // Cast buffer as Barometer Sensor Data
    struct sensor_event_baro *event = 
      (struct sensor_event_baro *) buffer;

    // Handle Pressure and Temperature
    printf(
      "%s: timestamp:%d value1:%.2f value2:%.2f\n",
      name, 
      event.timestamp, 
      event.pressure, 
      event.temperature
    );
  }
}

// Close the Sensor Device and free the buffer
close(fd);
free(buffer);

(Source)

NuttX compiles the Sensor Test App sensortest.c with this GCC command...

##  App Source Directory
cd $HOME/nuttx/apps/testing/sensortest

##  Compile sensortest.c with GCC
riscv64-unknown-elf-gcc \
  -c \
  -fno-common \
  -Wall \
  -Wstrict-prototypes \
  -Wshadow \
  -Wundef \
  -Os \
  -fno-strict-aliasing \
  -fomit-frame-pointer \
  -fstack-protector-all \
  -ffunction-sections \
  -fdata-sections \
  -g \
  -march=rv32imafc \
  -mabi=ilp32f \
  -mno-relax \
  -isystem "$HOME/nuttx/nuttx/include" \
  -D__NuttX__ \
  -DNDEBUG \
  -DARCH_RISCV  \
  -pipe \
  -I "$HOME/nuttx/apps/include" \
  -Dmain=sensortest_main \
  sensortest.c \
  -o  sensortest.c.home.user.nuttx.apps.testing.sensortest.o

(Observed from make --trace)

Let's convert the Sensor Test App from C to Zig...

Auto-Translate Sensor App to Zig

The Zig Compiler can auto-translate C code to Zig. (See this)

Here's how we auto-translate our Sensor App sensortest.c from C to Zig...

  • Take the GCC command from above

  • Change riscv64-unknown-elf-gcc to zig translate-c

  • Add the target -target riscv32-freestanding-none -mcpu=baseline_rv32-d

  • Remove -march=rv32imafc

  • Surround the C Flags by -cflags ... --

Like this...

##  App Source Directory
cd $HOME/nuttx/apps/testing/sensortest

##  Auto-translate sensortest.c from C to Zig
zig translate-c \
  -target riscv32-freestanding-none \
  -mcpu=baseline_rv32-d \
  -cflags \
    -fno-common \
    -Wall \
    -Wstrict-prototypes \
    -Wshadow \
    -Wundef \
    -Os \
    -fno-strict-aliasing \
    -fomit-frame-pointer \
    -fstack-protector-all \
    -ffunction-sections \
    -fdata-sections \
    -g \
    -mabi=ilp32f \
    -mno-relax \
  -- \
  -isystem "$HOME/nuttx/nuttx/include" \
  -D__NuttX__ \
  -DNDEBUG \
  -DARCH_RISCV  \
  -I "$HOME/nuttx/apps/include" \
  -Dmain=sensortest_main  \
  sensortest.c \
  >sensortest.zig

To fix the translation (Zig Translate doesn't support goto) we need to insert this...

#ifdef __clang__  //  Workaround for zig cc
#include <arch/types.h>
#include "../../nuttx/include/limits.h"
#define goto return ret;
#define name_err
#define open_err
#define ctl_err
#endif  //  __clang__

(Source)

And change this...

#ifndef __clang__  //  Workaround for NuttX with zig cc
ctl_err:
#endif  //  !__clang__
  close(fd);
#ifndef __clang__  //  Workaround for NuttX with zig cc
open_err:
#endif  //  !__clang__
  free(buffer);
#ifndef __clang__  //  Workaround for NuttX with zig cc
name_err:
#endif  //  !__clang__

(Source)

(See the changes)

Here's the original C code: sensortest.c

And the auto-translation from C to Zig: translated/sensortest.zig

Sensor App in Zig

We copy the relevant snippets from the Auto-Translation to create our Zig Sensor App: sensortest.zig

///////////////////////////////////////////////////////////////////////////////
// Main Function
/// Main Function that will be called by NuttX. We read the Sensor Data from a Sensor.
pub export fn sensortest_main(
argc: c_int,
argv: [*c]const [*c]u8
) c_int {
debug("Zig Sensor Test", .{});
var interval: c_uint = @bitCast(c_uint, @as(c_int, 1000000));
var received: c_uint = 0;
var latency: c_uint = 0;
var count: c_uint = 0;
var devname: [256]u8 = undefined;
var fds: c.struct_pollfd = undefined;
var buffer: [*c]u8 = undefined;
var name: [*c]u8 = undefined;
var len: c_int = 0;
var fd: c_int = undefined;
var idx: c_int = undefined;
var ret: c_int = undefined;
if (argc <= @as(c_int, 1)) {
usage();
return -@as(c_int, 22);
}
// TODO: cannot cast negative value -1 to unsigned integer type 'usize'
// if (c.signal(@as(c_int, 10), exit_handler) == @intToPtr(c._sa_handler_t, -@as(c_int, 1))) {
// return -c.__errno().*;
// }
g_should_exit = @as(c_int, 0) != 0;
while ((blk: {
const tmp = c.getopt(argc, argv, "i:b:n:h");
ret = tmp;
break :blk tmp;
}) != -@as(c_int, 1)) {
while (true) {
switch (ret) {
@as(c_int, 105) => {
interval = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
break;
},
@as(c_int, 98) => {
latency = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
break;
},
@as(c_int, 110) => {
count = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
break;
},
else => {
usage();
return ret;
},
}
break;
}
}
if (c.getoptindp().* < argc) {
name = (blk: {
const tmp = c.getoptindp().*;
if (tmp >= 0) break :blk argv + @intCast(usize, tmp) else break :blk argv - ~@bitCast(usize, @intCast(isize, tmp) +% -1);
}).*;
{
idx = 0;
while (@bitCast(c_uint, idx) < (@sizeOf([30]struct_sensor_info) / @sizeOf(struct_sensor_info))) : (idx += 1) {
if (!(c.strncmp(name, g_sensor_info[@intCast(c_uint, idx)].name, c.strlen(g_sensor_info[@intCast(c_uint, idx)].name)) != 0)) {
len = @bitCast(c_int, @as(c_uint, g_sensor_info[@intCast(c_uint, idx)].esize));
buffer = @ptrCast([*c]u8, @alignCast(@import("std").meta.alignment(u8), c.calloc(@bitCast(usize, @as(c_int, 1)), @bitCast(usize, len))));
break;
}
}
}
if (!(len != 0)) {
_ = printf("The sensor node name:%s is invalid\n", name);
usage();
ret = -@as(c_int, 22);
return ret;
}
if (!(buffer != null)) {
ret = -@as(c_int, 12);
return ret;
}
} else {
usage();
ret = -@as(c_int, 22);
return ret;
}
_ = c.snprintf(@ptrCast([*c]u8, @alignCast(@import("std").meta.alignment(u8), &devname)), @bitCast(usize, @as(c_int, 256)), "/dev/sensor/%s", name);
fd = c.open(@ptrCast([*c]u8, @alignCast(@import("std").meta.alignment(u8), &devname)), (@as(c_int, 1) << @intCast(@import("std").math.Log2Int(c_int), 0)) | (@as(c_int, 1) << @intCast(@import("std").math.Log2Int(c_int), 6)));
if (fd < @as(c_int, 0)) {
ret = -c.__errno().*;
_ = printf("Failed to open device:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(@import("std").meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
ret = c.ioctl(fd, @as(c_int, 2560) | @as(c_int, 129), &interval);
if (ret < @as(c_int, 0)) {
ret = -c.__errno().*;
if (ret != -@as(c_int, 134)) {
_ = printf("Failed to set interval for sensor:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(@import("std").meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
}
ret = c.ioctl(fd, @as(c_int, 2560) | @as(c_int, 130), &latency);
if (ret < @as(c_int, 0)) {
ret = -c.__errno().*;
if (ret != -@as(c_int, 134)) {
_ = printf("Failed to batch for sensor:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(@import("std").meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
}
ret = c.ioctl(fd, @as(c_int, 2560) | @as(c_int, 128), @as(c_int, 1));
if (ret < @as(c_int, 0)) {
ret = -c.__errno().*;
if (ret != -@as(c_int, 134)) {
_ = printf("Failed to enable sensor:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(@import("std").meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
}
_ = printf("SensorTest: Test %s with interval(%uus), latency(%uus)\n", @ptrCast([*c]u8, @alignCast(@import("std").meta.alignment(u8), &devname)), interval, latency);
fds.fd = fd;
fds.events = @bitCast(c.pollevent_t, @as(c_int, 1));
while ((!(count != 0) or (received < count)) and !g_should_exit) {
if (c.poll(&fds, @bitCast(c.nfds_t, @as(c_int, 1)), -@as(c_int, 1)) > @as(c_int, 0)) {
if (c.read(fd, @ptrCast(?*anyopaque, buffer), @bitCast(usize, len)) >= len) {
received +%= 1;
g_sensor_info[@intCast(c_uint, idx)].print.?(buffer, name);
}
}
}
_ = printf("SensorTest: Received message: %s, number:%d/%d\n", name, received, count);
ret = c.ioctl(fd, @as(c_int, 2560) | @as(c_int, 128), @as(c_int, 0));
if (ret < @as(c_int, 0)) {
ret = -c.__errno().*;
_ = printf("Failed to disable sensor:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(@import("std").meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
_ = c.close(fd);
c.free(@ptrCast(?*anyopaque, buffer));
c.getoptindp().* = 0;
return ret;
}
pub fn print_vec3(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_accel = @intToPtr([*c]c.struct_sensor_event_accel, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu x:%.2f y:%.2f z:%.2f, temperature:%.2f\n", name, event.*.timestamp, @floatCast(f64, event.*.x), @floatCast(f64, event.*.y), @floatCast(f64, event.*.z), @floatCast(f64, event.*.temperature));
}
pub fn print_valf3(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_rgb = @intToPtr([*c]c.struct_sensor_event_rgb, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu value1:%.2f value2:%.2f, value3:%.2f\n", name, event.*.timestamp, @floatCast(f64, event.*.r), @floatCast(f64, event.*.g), @floatCast(f64, event.*.b));
}
pub fn print_valf2(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_baro = @intToPtr([*c]c.struct_sensor_event_baro, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu value1:%.2f value2:%.2f\n", name, event.*.timestamp, @floatCast(f64, event.*.pressure), @floatCast(f64, event.*.temperature));
}
pub fn print_valf(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_prox = @intToPtr([*c]c.struct_sensor_event_prox, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu value:%.2f\n", name, event.*.timestamp, @floatCast(f64, event.*.proximity));
}
pub fn print_valb(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_hall = @intToPtr([*c]c.struct_sensor_event_hall, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu value:%d\n", name, event.*.timestamp, @as(c_int, @boolToInt(event.*.hall)));
}
pub fn print_vali2(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_ots = @intToPtr([*c]c.struct_sensor_event_ots, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu value1:% li value2:% li\n", name, event.*.timestamp, event.*.x, event.*.y);
}
pub fn print_ppgd(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_ppgd = @intToPtr([*c]c.struct_sensor_event_ppgd, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu ppg1:%lu ppg2:%lu current:%lu gain1:%u gain2:%u\n", name, event.*.timestamp, event.*.ppg[@intCast(c_uint, @as(c_int, 0))], event.*.ppg[@intCast(c_uint, @as(c_int, 1))], event.*.current, @bitCast(c_int, @as(c_uint, event.*.gain[@intCast(c_uint, @as(c_int, 0))])), @bitCast(c_int, @as(c_uint, event.*.gain[@intCast(c_uint, @as(c_int, 1))])));
}
pub fn print_ppgq(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_ppgq = @intToPtr([*c]c.struct_sensor_event_ppgq, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu ppg1:%lu ppg2:%lu ppg3:%lu ppg4:%lu current:%lu gain1:%u gain2:%u gain3:%u gain4:%u\n", name, event.*.timestamp, event.*.ppg[@intCast(c_uint, @as(c_int, 0))], event.*.ppg[@intCast(c_uint, @as(c_int, 1))], event.*.ppg[@intCast(c_uint, @as(c_int, 2))], event.*.ppg[@intCast(c_uint, @as(c_int, 3))], event.*.current, @bitCast(c_int, @as(c_uint, event.*.gain[@intCast(c_uint, @as(c_int, 0))])), @bitCast(c_int, @as(c_uint, event.*.gain[@intCast(c_uint, @as(c_int, 1))])), @bitCast(c_int, @as(c_uint, event.*.gain[@intCast(c_uint, @as(c_int, 2))])), @bitCast(c_int, @as(c_uint, event.*.gain[@intCast(c_uint, @as(c_int, 3))])));
}
pub fn print_gps(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_gps = @intToPtr([*c]c.struct_sensor_event_gps, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu time_utc: %llu latitude: %f longitude: %f altitude: %f altitude_ellipsoid: %f eph: %f epv: %f hdop: %f vdop: %f ground_speed: %f course: %f satellites_used: %lu\n", name, event.*.timestamp, event.*.time_utc, @floatCast(f64, event.*.latitude), @floatCast(f64, event.*.longitude), @floatCast(f64, event.*.altitude), @floatCast(f64, event.*.altitude_ellipsoid), @floatCast(f64, event.*.eph), @floatCast(f64, event.*.epv), @floatCast(f64, event.*.hdop), @floatCast(f64, event.*.vdop), @floatCast(f64, event.*.ground_speed), @floatCast(f64, event.*.course), event.*.satellites_used);
}
pub fn print_gps_satellite(arg_buffer: [*c]const u8, arg_name: [*c]const u8) callconv(.C) void {
var buffer = arg_buffer;
var name = arg_name;
var event: [*c]c.struct_sensor_event_gps_satellite = @intToPtr([*c]c.struct_sensor_event_gps_satellite, @ptrToInt(buffer));
_ = printf("%s: timestamp: %llu count: %lu satellites: %lu\n", name, event.*.timestamp, event.*.count, event.*.satellites);
}
pub fn usage() callconv(.C) void {
_ = printf("sensortest [arguments...] <command>\n");
_ = printf("\t[-h ] sensortest commands help\n");
_ = printf("\t[-i <val>] The output data period of sensor in us\n");
_ = printf("\t default: 1000000\n");
_ = printf("\t[-b <val>] The maximum report latency of sensor in us\n");
_ = printf("\t default: 0\n");
_ = printf("\t[-n <val>] The number of output data\n");
_ = printf("\t default: 0\n");
_ = printf(" Commands:\n");
_ = printf("\t<sensor_node_name> ex, accel0(/dev/sensor/accel0)\n");
}
pub fn exit_handler(arg_signo: c_int) callconv(.C) void {
var signo = arg_signo;
_ = signo;
g_should_exit = @as(c_int, 1) != 0;
}
pub const g_sensor_info: [30]struct_sensor_info = [30]struct_sensor_info{
struct_sensor_info{
.print = print_vec3,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_accel))),
.name = "accel",
},
struct_sensor_info{
.print = print_vec3,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_mag))),
.name = "mag",
},
struct_sensor_info{
.print = print_vec3,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_gyro))),
.name = "gyro",
},
struct_sensor_info{
.print = print_valf2,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_baro))),
.name = "baro",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_light))),
.name = "light",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_prox))),
.name = "prox",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_humi))),
.name = "humi",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_temp))),
.name = "temp",
},
struct_sensor_info{
.print = print_valf3,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_rgb))),
.name = "rgb",
},
struct_sensor_info{
.print = print_valb,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_hall))),
.name = "hall",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_ir))),
.name = "ir",
},
struct_sensor_info{
.print = print_gps,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_gps))),
.name = "gps",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_uv))),
.name = "uv",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_noise))),
.name = "noise",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_pm25))),
.name = "pm25",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_pm1p0))),
.name = "pm1p0",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_pm10))),
.name = "pm10",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_co2))),
.name = "co2",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_hcho))),
.name = "hcho",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_tvoc))),
.name = "tvoc",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_ph))),
.name = "ph",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_dust))),
.name = "dust",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_hrate))),
.name = "hrate",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_hbeat))),
.name = "hbeat",
},
struct_sensor_info{
.print = print_valf,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_ecg))),
.name = "ecg",
},
struct_sensor_info{
.print = print_ppgd,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_ppgd))),
.name = "ppgd",
},
struct_sensor_info{
.print = print_ppgq,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_ppgq))),
.name = "ppgq",
},
struct_sensor_info{
.print = print_valf2,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_impd))),
.name = "impd",
},
struct_sensor_info{
.print = print_vali2,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_ots))),
.name = "ots",
},
struct_sensor_info{
.print = print_gps_satellite,
.esize = @bitCast(u8, @truncate(u8, @sizeOf(c.struct_sensor_event_gps_satellite))),
.name = "gps_satellite",
},
};
pub const struct_sensor_info = extern struct {
print: data_print,
esize: u8,
name: [*c]const u8,
};
pub var g_should_exit: bool = @as(c_int, 0) != 0;
pub const data_print = ?fn ([*c]const u8, [*c]const u8) callconv(.C) void;

Then we compile our Zig Sensor App...

##  Download our Zig Sensor App for NuttX
git clone --recursive https://github.com/lupyuen/visual-zig-nuttx
cd visual-zig-nuttx

##  Compile the Zig App for BL602 (RV32IMACF with Hardware Floating-Point)
zig build-obj \
  --verbose-cimport \
  -target riscv32-freestanding-none \
  -mcpu=baseline_rv32-d \
  -isystem "$HOME/nuttx/nuttx/include" \
  -I "$HOME/nuttx/apps/include" \
  sensortest.zig

##  Patch the ELF Header of `sensortest.o` from Soft-Float ABI to Hard-Float ABI
xxd -c 1 sensortest.o \
  | sed 's/00000024: 01/00000024: 03/' \
  | xxd -r -c 1 - sensortest2.o
cp sensortest2.o sensortest.o

##  Copy the compiled app to NuttX and overwrite `sensortest.o`
##  TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cp sensortest.o $HOME/nuttx/apps/testing/sensortest/sensortest*.o

##  Build NuttX to link the Zig Object from `sensortest.o`
##  TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cd $HOME/nuttx/nuttx
make

Connect BME280 Sensor

For testing the Zig Sensor App, we connect the BME280 Sensor (Temperature / Humidity / Air Pressure) to Pine64's PineCone BL602 Board...

BL602 Pin BME280 Pin Wire Colour
GPIO 1 SDA Green
GPIO 2 SCL Blue
3V3 3.3V Red
GND GND Black

Pine64 PineCone BL602 RISC-V Board connected to Bosch BME280 Sensor

Run Zig Sensor App

To read the BME280 Sensor, let's run the Zig Sensor App: sensortest.zig

First we read the Humidity with our Zig Sensor App...

NuttShell (NSH) NuttX-10.3.0
nsh> sensortest -n 1 humi0
Zig Sensor Test
bme280_fetch: temperature=30.520000 °C, pressure=1027.211548 mbar, humidity=72.229492 %
humi0: timestamp:109080000 value:72.23

(See the complete log)

Our Zig App returns the correct Humidity (value): 72 %.

Next we read the Temperature and Air Pressure with our Zig Sensor App...

nsh> sensortest -n 1 baro0
Zig Sensor Test
bme280_fetch: temperature=30.520000 °C, pressure=1029.177490 mbar, humidity=72.184570 %
baro0: timestamp:78490000 value1:72.18 value2:72.18

(See the complete log)

This shows that the BME280 Driver has fetched the correct Temperature (30 °C), Pressure (1,029 mbar) and Humidity (72 %).

But our Zig App returns both Pressure (value1) and Temperature (value2) as 72. Which is incorrect.

Compare the above output with the original C Version of the Sensor App: sensortest.c

nsh> sensortest -n 1 humi0
bme280_fetch: temperature=30.690001 °C, pressure=1027.283447 mbar, humidity=72.280273 %
humi0: timestamp:26000000 value:72.28

nsh> sensortest -n 1 baro0
bme280_fetch: temperature=30.690001 °C, pressure=1029.177368 mbar, humidity=72.257813 %
baro0: timestamp:14280000 value1:1029.18 value2:30.69

(See the complete log)

We see that Humidity (value), Pressure (value1) and Temperature (value2) are returned correctly by the C Version of the Sensor App.

Something got messed up in the Auto-Translation from C (sensortest.c) to Zig (sensortest.zig). Let's find out why...

Fix Floating-Point Values

(Note: We observed this issue with Zig Compiler version 0.10.0, it might have been fixed in later versions of the compiler)

Earlier we saw that our Zig Sensor App printed the incorrect Sensor Values for Pressure (value1) and Temperature (value2)...

nsh> sensortest -n 1 baro0
Zig Sensor Test
bme280_fetch: temperature=30.520000 °C, pressure=1029.177490 mbar, humidity=72.184570 %
baro0: timestamp:78490000 value1:72.18 value2:72.18

Zig seems to have a problem passing the Pressure and Temperature values (both f32) to printf, based on this Auto-Translated Zig Code...

fn print_valf2(buffer: [*c]const u8, name: [*c]const u8) void {
    const event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
    _ = printf("%s: timestamp:%llu value1:%.2f value2:%.2f\n", 
       name, 
       event.*.timestamp, 
       @floatCast(f64, event.*.pressure), 
       @floatCast(f64, event.*.temperature)
    );
}

(Source)

The workaround is to convert the Float values to Integer AND split into two calls to printf...

fn print_valf2(buffer: [*c]const u8, name: [*c]const u8) void {
    const event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
    _ = printf("%s: timestamp:%llu ", 
        name, 
        event.*.timestamp, 
    );
    _ = printf("value1:%d value2:%d\n", 
        @floatToInt(i32, event.*.pressure), 
        @floatToInt(i32, event.*.temperature)
    );
}

(Source)

Now our Zig Sensor App prints the correct values, but truncated as Integers...

nsh> sensortest -n 1 baro0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_baro0 with interval(1000000us), latency(0us)
baro0: timestamp:42610000 value1:1003 value2:31
SensorTest: Received message: baro0, number:1/1

nsh> sensortest -n 1 humi0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_humi0 with interval(1000000us), latency(0us)
humi0: timestamp:32420000 value:68
SensorTest: Received message: humi0, number:1/1

Since printf works OK with Integers, let's print the Floats as Integers with 2 decimal places...

/// Print the float with 2 decimal places.
/// We print as integers because `printf` has a problem with floats.
fn print_float(f: f32) void {
    const scaled = @floatToInt(i32, f * 100);
    const f1 = @divTrunc(scaled, 100);
    const f2 = @mod(scaled, 100);
    _ = printf("%d.%02d", f1, f2);
}

(Source)

Then we pass the Floats to print_float for printing...

fn print_valf2(buffer: [*c]const u8, name: [*c]const u8) void {
    const event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
    _ = printf("%s: timestamp:%llu ", 
        name, 
        event.*.timestamp, 
    );
    _ = printf("value1:");  print_float(event.*.pressure);
    _ = printf(" value2:"); print_float(event.*.temperature);
    _ = printf("\n");
}

(Source)

Finally we see the Pressure (value1), Temperature (value2) and Humidity (value) printed correctly with 2 decimal places!

nsh> sensortest -n 1 baro0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_baro0 with interval(1000000us), latency(0us)
baro0: timestamp:17780000 value1:1006.12 value2:29.65
SensorTest: Received message: baro0, number:1/1

nsh> sensortest -n 1 humi0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_humi0 with interval(1000000us), latency(0us)
humi0: timestamp:28580000 value:77.44
SensorTest: Received message: humi0, number:1/1

Have we tried other options for @floatCast?

Yes we tested these options for @floatCast...

_ = printf("value1 no floatCast: %f\n",  event.*.pressure);
_ = printf("value1 floatCast f32: %f\n", @floatCast(f32, event.*.pressure));
_ = printf("value1 floatCast f64: %f\n", @floatCast(f64, event.*.pressure));

But the result is incorrect...

nsh> sensortest -n 1 baro0
Zig Sensor Test
baro0: timestamp:60280000 value1:1006.90 value2:30.79
value1 no floatCast: 0.000000
value1 floatCast f32: 0.000000
value1 floatCast f64: 30.790001

Because value1 (Pressure) is supposed to be 1006, not 30.

Robert Lipe suggests that we might have a problem with the RISC-V Calling Conventions for Floats and Doubles...

The calling conventions for RISC-V on small computers for floats/doubles is weird and the software/emulators/ libs are confusing. Verify that floats get promoted to doubles in the call and that they go to F0, F2, F4, etc. in the frame, properly aligned. Double check complr flags

(Source)

See, e.g., https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-cc.adoc and related. This is totally the kind of thing that small parts (enable FP on BL602; disable s/w libs) and x-language can get wrong.

(Source)

Instead of printf, why not call the Zig Debug Logger debug?

debug("pressure: {}", .{ event.*.pressure });
debug("temperature: {}", .{ event.*.temperature });

This causes a Linker Error, as explained below...

Floating-Point Link Error

(Note: We observed this issue with Zig Compiler version 0.10.0, it might have been fixed in later versions of the compiler)

When we call the Zig Debug Logger debug to print Floating-Point Numbers (32-bit)...

var event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
debug("pressure: {}",    .{ event.*.pressure });
debug("temperature: {}", .{ event.*.temperature });

It fails to link...

riscv64-unknown-elf-ld: nuttx/nuttx/staging/libapps.a(sensortest.c.home.user.nuttx.apps.testing.sensortest.o): in function `std.fmt.errol.errolInt':
zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:305: 
undefined reference to `__fixunsdfti'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:305: 
undefined reference to `__floatuntidf'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:315: 
undefined reference to `__umodti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:316: undefined reference to `__udivti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:316: 
undefined reference to `__umodti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:318: 
undefined reference to `__umodti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:319: 
undefined reference to `__udivti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:319: 
undefined reference to `__umodti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:324: 
undefined reference to `__udivti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:335: 
undefined reference to `__udivti3'
riscv64-unknown-elf-ld: nuttx/nuttx/staging/libapps.a(sensortest.c.home.user.nuttx.apps.testing.sensortest.o): in function `std.fmt.errol.fpeint':
zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:677: 
undefined reference to `__ashlti3'

But printing as Integers with debug works OK...

debug("pressure: {}", 
  .{ @floatToInt(i32, event.*.pressure) });
debug("temperature: {}", 
  .{ @floatToInt(i32, event.*.temperature) });

So we print Floats as Integers with the Debug Logger...

Fixed-Point Printing

Earlier we saw that Zig Debug Logger debug won't print Floating-Point Numbers. (Due to a Linker Error)

Let's convert Floating-Point Numbers to Fixed-Point Numbers (2 decimal places) and print as Integers instead...

/// Convert the float to a fixed-point number (`int`.`frac`) with 2 decimal places.
/// We do this because `debug` has a problem with floats.
pub fn floatToFixed(f: f32) struct { int: i32, frac: u8 } {
    const scaled = @floatToInt(i32, f * 100.0);
    const rem = @rem(scaled, 100);
    const rem_abs = if (rem < 0) -rem else rem;
    return .{
        .int  = @divTrunc(scaled, 100),
        .frac = @intCast(u8, rem_abs),
    };
}

(Source)

This is how we print Floating-Point Numbers as Fixed-Point Numbers...

/// Print 2 floats
fn print_valf2(buffer: []const align(8) u8, name: []const u8) void {
    _ = name;
    const event = @ptrCast(*const c.struct_sensor_baro, &buffer[0]);
    const pressure = floatToFixed(event.*.pressure);
    const temperature = floatToFixed(event.*.temperature);
    debug("value1:{}.{:0>2}", .{ pressure.int, pressure.frac });
    debug("value2:{}.{:0>2}", .{ temperature.int, temperature.frac });
}

(Source)

Change to Static Buffer

Originally we allocated the Sensor Data Buffer from the Heap via calloc...

// Allocate buffer from heap
len = @bitCast(c_int, @as(c_uint, g_sensor_info[@intCast(c_uint, idx)].esize));
buffer = @ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), 
    c.calloc(@bitCast(usize, @as(c_int, 1)), @bitCast(usize, len))));
...
// Read into heap buffer
if (c.read(fd, @ptrCast(?*anyopaque, buffer), @bitCast(usize, len)) >= len) {
    g_sensor_info[@intCast(c_uint, idx)].print.?(buffer, name);
}
...
// Deallocate heap buffer
c.free(@ptrCast(?*anyopaque, buffer));

To make this a little safer, we switched to a Static Buffer...

/// Sensor Data Buffer
/// (Aligned to 8 bytes because it's passed to C)
var sensor_data align(8) = std.mem.zeroes([256]u8);

(Source)

So that we no longer worry about deallocating the buffer...

// Use static buffer
len = @bitCast(c_int, @as(c_uint, g_sensor_info[@intCast(c_uint, idx)].esize));
assert(sensor_data.len >= len);
...
// Read into static buffer
if (c.read(fd, @ptrCast(?*anyopaque, &sensor_data), @bitCast(usize, len)) >= len) {
    g_sensor_info[@intCast(c_uint, idx)].print.?(&sensor_data, name);
}
...
// No need to deallocate buffer

(Source)

We also moved the Device Name from the Stack...

/// Main Function that will be called by NuttX. We read the Sensor Data from a Sensor.
pub export fn sensortest_main(...) {
    var devname: [c.PATH_MAX]u8 = undefined;

(Source)

To a Static Buffer...

/// Device Name, like "/dev/uorb/sensor_baro0" or "/dev/uorb/sensor_humi0"
/// (Aligned to 8 bytes because it's passed to C)
var devname align(8) = std.mem.zeroes([c.PATH_MAX]u8);

(Source)

And we removed the Alignment from Device Name...

fd = c.open(
    @ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)), 
    c.O_RDONLY | c.O_NONBLOCK
);

(Source)

So it looks like this...

fd = c.open(
    &devname[0], 
    c.O_RDONLY | c.O_NONBLOCK
);

(Source)

Incorrect Alignment

When we align the Sensor Data Buffer to 4 bytes...

/// Sensor Data Buffer
/// (Aligned to 4 bytes because it's passed to C)
var sensor_data align(4) = std.mem.zeroes([256]u8);

It triggers a Zig Panic due to Incorrect Aligment...

nsh> sensortest -n 1 baro0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_baro0 with interval(1000000us), latency(0us)

!ZIG PANIC!
incorrect alignment
Stack Trace:
0x23014f4c
0x230161e2

RISC-V Disassembly shows that it's checking andi a0,a0,7...

nuttx/visual-zig-nuttx/sensortest.zig:196
    const event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
23014f2a: 85aa     mv      a1,a0
23014f2c: fcb42e23 sw      a1,-36(s0)
23014f30: 891d     andi    a0,a0,7
23014f32: 4581     li      a1,0
23014f34: 00b50c63 beq     a0,a1,23014f4c <print_valf2+0x32>
23014f38: a009     j       23014f3a <print_valf2+0x20>
23014f3a: 23068537 lui     a0,0x23068
23014f3e: 3c850513 addi    a0,a0,968 # 230683c8 <__unnamed_6>
23014f42: 4581     li      a1,0
23014f44: 00000097 auipc   ra,0x0
23014f48: dd4080e7 jalr    -556(ra) # 23014d18 <panic>
23014f4c: fdc42503 lw      a0,-36(s0)
23014f50: fea42a23 sw      a0,-12(s0)

(Last 3 bits of address must be 0)

Which means that the buffer address must be aligned to 8 bytes...

/// Sensor Data Buffer
/// (Aligned to 8 bytes because it's passed to C)
var sensor_data align(8) = std.mem.zeroes([256]u8);

(Source)

Probably because struct_sensor_baro contains a timestamp field that's a 64-bit Integer.

Clean Up

The Auto-Translation from C to Zig looks super verbose and strips away the Macro Constants...

// Parse the Command-Line Options
g_should_exit = @as(c_int, 0) != 0;
while ((blk: {
    const tmp = c.getopt(argc, argv, "i:b:n:h");
    ret = tmp;
    break :blk tmp;
}) != -@as(c_int, 1)) {
    while (true) {
        switch (ret) {
            @as(c_int, 105) => {
                interval = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
                break;
            },
            @as(c_int, 98) => {
                latency = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
                break;
            },
            @as(c_int, 110) => {
                count = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
                break;
            },
            else => {
                usage();
                return ret;
            },
        }
        break;
    }
}

(Source)

We clean up the Zig code like this...

// Parse the Command-Line Options
g_should_exit = false;
while (true) {
    ret = c.getopt(argc, argv, "i:b:n:h");
    if (ret == c.EOF) { break; }
    switch (ret) {
        'i' => { interval = c.strtoul(c.getoptargp().*, null, 0); },
        'b' => { latency  = c.strtoul(c.getoptargp().*, null, 0); },
        'n' => { count    = c.strtoul(c.getoptargp().*, null, 0); },
        else => { usage(); return ret; },
    }
}

(Source)

So that it resembles the original C code...

g_should_exit = false;
while ((ret = getopt(argc, argv, "i:b:n:h")) != EOF)
  {
    switch (ret)
      {
        case 'i':
          interval = strtoul(optarg, NULL, 0);
          break;

        case 'b':
          latency = strtoul(optarg, NULL, 0);
          break;

        case 'n':
          count = strtoul(optarg, NULL, 0);
          break;

        case 'h':
        default:
          usage();
          goto name_err;
      }
  }

(Source)

Our Zig Sensor App was originally this...

/// Main Function that will be called by NuttX. We read the Sensor Data from a Sensor.
pub export fn sensortest_main(
argc: c_int,
argv: [*c]const [*c]u8
) c_int {
debug("Zig Sensor Test", .{});
var interval: c_uint = @bitCast(c_uint, @as(c_int, 1000000));
var received: c_uint = 0;
var latency: c_uint = 0;
var count: c_uint = 0;
var devname: [256]u8 = undefined;
var fds: c.struct_pollfd = undefined;
var name: [*c]u8 = undefined;
var len: c_int = 0;
var fd: c_int = undefined;
var idx: c_int = undefined;
var ret: c_int = undefined;
if (argc <= @as(c_int, 1)) {
usage();
return -@as(c_int, 22);
}
// TODO: cannot cast negative value -1 to unsigned integer type 'usize'
// if (c.signal(@as(c_int, 10), exit_handler) == @intToPtr(c._sa_handler_t, -@as(c_int, 1))) {
// return -c.__errno().*;
// }
g_should_exit = @as(c_int, 0) != 0;
while ((blk: {
const tmp = c.getopt(argc, argv, "i:b:n:h");
ret = tmp;
break :blk tmp;
}) != -@as(c_int, 1)) {
while (true) {
switch (ret) {
@as(c_int, 105) => {
interval = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
break;
},
@as(c_int, 98) => {
latency = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
break;
},
@as(c_int, 110) => {
count = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
break;
},
else => {
usage();
return ret;
},
}
break;
}
}
if (c.getoptindp().* < argc) {
name = (blk: {
const tmp = c.getoptindp().*;
if (tmp >= 0) break :blk argv + @intCast(usize, tmp) else break :blk argv - ~@bitCast(usize, @intCast(isize, tmp) +% -1);
}).*;
{
idx = 0;
while (@bitCast(c_uint, idx) < (@sizeOf([30]struct_sensor_info) / @sizeOf(struct_sensor_info))) : (idx += 1) {
if (!(c.strncmp(name, g_sensor_info[@intCast(c_uint, idx)].name, c.strlen(g_sensor_info[@intCast(c_uint, idx)].name)) != 0)) {
len = @bitCast(c_int, @as(c_uint, g_sensor_info[@intCast(c_uint, idx)].esize));
assert(sensor_data.len >= len);
break;
}
}
}
if (!(len != 0)) {
_ = printf("The sensor node name:%s is invalid\n", name);
usage();
ret = -@as(c_int, 22);
return ret;
}
} else {
usage();
ret = -@as(c_int, 22);
return ret;
}
_ = c.snprintf(@ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)), @bitCast(usize, @as(c_int, 256)), "/dev/sensor/%s", name);
fd = c.open(@ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)), (@as(c_int, 1) << @intCast(std.math.Log2Int(c_int), 0)) | (@as(c_int, 1) << @intCast(std.math.Log2Int(c_int), 6)));
if (fd < @as(c_int, 0)) {
ret = -c.__errno().*;
_ = printf("Failed to open device:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
ret = c.ioctl(fd, @as(c_int, 2560) | @as(c_int, 129), &interval);
if (ret < @as(c_int, 0)) {
ret = -c.__errno().*;
if (ret != -@as(c_int, 134)) {
_ = printf("Failed to set interval for sensor:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
}
ret = c.ioctl(fd, @as(c_int, 2560) | @as(c_int, 130), &latency);
if (ret < @as(c_int, 0)) {
ret = -c.__errno().*;
if (ret != -@as(c_int, 134)) {
_ = printf("Failed to batch for sensor:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
}
ret = c.ioctl(fd, @as(c_int, 2560) | @as(c_int, 128), @as(c_int, 1));
if (ret < @as(c_int, 0)) {
ret = -c.__errno().*;
if (ret != -@as(c_int, 134)) {
_ = printf("Failed to enable sensor:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
}
_ = printf("SensorTest: Test %s with interval(%uus), latency(%uus)\n", @ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)), interval, latency);
fds.fd = fd;
fds.events = @bitCast(c.pollevent_t, @as(c_int, 1));
while ((!(count != 0) or (received < count)) and !g_should_exit) {
if (c.poll(&fds, @bitCast(c.nfds_t, @as(c_int, 1)), -@as(c_int, 1)) > @as(c_int, 0)) {
if (c.read(fd, @ptrCast(?*anyopaque, &sensor_data), @bitCast(usize, len)) >= len) {
received +%= 1;
g_sensor_info[@intCast(c_uint, idx)].print.?(&sensor_data, name);
}
}
}
_ = printf("SensorTest: Received message: %s, number:%d/%d\n", name, received, count);
ret = c.ioctl(fd, @as(c_int, 2560) | @as(c_int, 128), @as(c_int, 0));
if (ret < @as(c_int, 0)) {
ret = -c.__errno().*;
_ = printf("Failed to disable sensor:%s, ret:%s\n", @ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)), c.strerror(c.__errno().*));
return ret;
}
debug("close", .{});
_ = c.close(fd);
debug("getoptindp", .{});
c.getoptindp().* = 0;
debug("return", .{});
return ret;
}

After cleanup becomes this...

/// Read the Sensor Data from a Sensor specified by the Command-Line Options
pub fn test_multisensor(
argc: c_int,
argv: [*c]const [*c]u8
) !void {
debug("test_multisensor", .{});
// Register the Signal Handler for Ctrl-C
const handler = c.signal(c.SIGINT, exit_handler);
if (@ptrToInt(handler) == @ptrToInt(c.SIG_ERR)) {
std.log.err("Failed to register signal handler", .{});
return error.SignalError;
}
// Parse the Command-Line Options
g_should_exit = false;
var interval: c_uint = 1_000_000;
var latency: c_uint = 0;
var count: c_uint = 0;
var ret: c_int = undefined;
while (true) {
ret = c.getopt(argc, argv, "i:b:n:h");
if (ret == c.EOF) { break; }
switch (ret) {
'i' => { interval = c.strtoul(c.getoptargp().*, null, 0); },
'b' => { latency = c.strtoul(c.getoptargp().*, null, 0); },
'n' => { count = c.strtoul(c.getoptargp().*, null, 0); },
else => { return error.OptionError; },
}
}
// Get Sensor Type
var name: []const u8 = undefined;
var len: usize = 0;
if (c.getoptindp().* < argc) {
const i = @intCast(usize, c.getoptindp().*);
name = std.mem.span(argv[i]);
} else {
return error.OptionError;
}
// Lookup Sensor Info
var idx: usize = 0;
for (g_sensor_info) |sensor, i| {
if (std.mem.startsWith(
u8,
name,
sensor.name
)) {
idx = i;
len = sensor.esize;
assert(sensor_data.len >= len);
break;
}
}
if (len == 0) {
std.log.err("The sensor node name:{s} is invalid", .{ name });
return error.NameError;
}
// Compose the Device Name. devname looks like "/dev/sensor/baro0" or "/dev/sensor/humi0"
const devname = std.fmt.bufPrint(
&devname_buffer,
"/dev/sensor/{s}\x00",
.{ name }
) catch { std.log.err("Path overflow", .{}); return error.OpenError; };
// Open the Sensor Device
const fd = c.open(
&devname[0],
c.O_RDONLY | c.O_NONBLOCK
);
if (fd < 0) {
std.log.err("Failed to open device:{s}, ret:{s}", .{ devname, c.strerror(errno()) });
return error.OpenError;
}
// Set Standby Interval
// TODO: Remove this definition when SNIOC_SET_INTERVAL has been been fixed: https://github.com/apache/incubator-nuttx/issues/6642
const SNIOC_SET_INTERVAL = c._SNIOC(0x0081);
ret = c.ioctl(fd, SNIOC_SET_INTERVAL, &interval);
if (ret < 0 and errno() != c.ENOTSUP) {
std.log.err("Failed to set interval for sensor:{s}, ret:{s}", .{ devname, c.strerror(errno()) });
return error.IntervalError;
}
// Set Batch Latency
ret = c.ioctl(fd, c.SNIOC_BATCH, &latency);
if (ret < 0 and errno() != c.ENOTSUP) {
std.log.err("Failed to batch for sensor:{s}, ret:{s}", .{ devname, c.strerror(errno()) });
return error.BatchError;
}
// Enable Sensor and switch to Normal Power Mode
ret = c.ioctl(fd, c.SNIOC_ACTIVATE, @as(c_int, 1));
if (ret < 0 and errno() != c.ENOTSUP) {
std.log.err("Failed to enable sensor:{s}, ret:{s}", .{ devname, c.strerror(errno()) });
return error.EnableError;
}
// Prepare to poll Sensor
debug("SensorTest: Test {s} with interval({}), latency({})", .{ devname, interval, latency });
var fds = std.mem.zeroes(c.struct_pollfd);
fds.fd = fd;
fds.events = c.POLLIN;
// Repeat until all samples have been read
var received: c_uint = 0;
while ((!(count != 0) or (received < count)) and !g_should_exit) {
// If Sensor Data is available...
if (c.poll(&fds, 1, -1) > 0) {
// Read the Sensor Data
if (c.read(fd, &sensor_data, len) >= len) {
// Print the Sensor Data
received += 1;
const sensor = g_sensor_info[idx];
sensor.print(&sensor_data, name);
}
}
}
debug("SensorTest: Received message: {s}, number:{}/{}", .{ name, received, count });
// Disable Sensor and switch to Low Power Mode
ret = c.ioctl(fd, c.SNIOC_ACTIVATE, @as(c_int, 0));
if (ret < 0) {
std.log.err("Failed to disable sensor:{s}, ret:{s}", .{ devname, c.strerror(errno()) });
return error.DisableError;
}
// Close the Sensor Device
_ = c.close(fd);
c.getoptindp().* = 0;
}

We test again to be sure that the Zig Sensor App is still working OK...

NuttShell (NSH) NuttX-10.3.0
nsh> uname -a
NuttX 10.3.0 32c8fdf272 Jul 18 2022 16:38:47 risc-v bl602evb

nsh> sensortest -n 1 baro0
Zig Sensor Test
test_multisensor
SensorTest: Test /dev/uorb/sensor_baro0  with interval(1000000), latency(0)
value1:1007.65
value2:27.68
SensorTest: Received message: baro0, number:1/1

nsh> sensortest -n 1 humi0
Zig Sensor Test
test_multisensor
SensorTest: Test /dev/uorb/sensor_humi0  with interval(1000000), latency(0)
value:78.91
SensorTest: Received message: humi0, number:1/1

We also check that errors are handled correctly...

nsh> sensortest -n 1 baro
Zig Sensor Test
test_multisensor
Failed to open device:/dev/uorb/sensor_baro , ret:No such file or directory

nsh> sensortest -n 1 invalid
Zig Sensor Test
test_multisensor
The sensor node name:invalid is invalid
sensortest test
 Test barometer sensor (/dev/uorb/sensor_baro0)
sensortest test2
 Test humidity sensor (/dev/uorb/sensor_humi0)
sensortest [arguments...] <command>
        [-h      ]  sensortest commands help
        [-i <val>]  The output data period of sensor in us
                    default: 1000000
        [-b <val>]  The maximum report latency of sensor in us
                    default: 0
        [-n <val>]  The number of output data
                    default: 0
 Commands:
        <sensor_node_name> ex, accel0(/dev/uorb/sensor_accel0)

nsh> sensortest
Zig Sensor Test
sensortest test
 Test barometer sensor (/dev/uorb/sensor_baro0)
sensortest test2
 Test humidity sensor (/dev/uorb/sensor_humi0)
sensortest [arguments...] <command>
        [-h      ]  sensortest commands help
        [-i <val>]  The output data period of sensor in us
                    default: 1000000
        [-b <val>]  The maximum report latency of sensor in us
                    default: 0
        [-n <val>]  The number of output data
                    default: 0
 Commands:
        <sensor_node_name> ex, accel0(/dev/uorb/sensor_accel0)

Read Barometer Sensor

Reading Sensor Data from a single sensor looks a lot simpler (because we don't need to cast the Sensor Data)...

// Omitted: Open the Sensor Device, enable the Sensor and poll for Sensor Data
...
// Define the Sensor Data Type
var sensor_data = std.mem.zeroes(c.struct_sensor_baro);
const len = @sizeOf(@TypeOf(sensor_data));

// Read the Sensor Data
if (c.read(fd, &sensor_data, len) >= len) {

    // Convert the Sensor Data to Fixed-Point Numbers
    const pressure    = floatToFixed(sensor_data.pressure);
    const temperature = floatToFixed(sensor_data.temperature);

    // Print the Sensor Data
    debug("pressure:{}.{:0>2}", .{
        pressure.int, 
        pressure.frac 
    });
    debug("temperature:{}.{:0>2}", .{
        temperature.int,
        temperature.frac 
    });
}

(Source)

Here's the Air Pressure and Temperature read from the BME280 Barometer Sensor...

nsh> sensortest test
Zig Sensor Test
test_sensor
pressure:1007.66
temperature:27.70

Read Humidity Sensor

To read a Humidity Sensor (like BME280), the code looks highly similar...

// Define the Sensor Data Type
var sensor_data = std.mem.zeroes(c.struct_sensor_humi);
const len = @sizeOf(@TypeOf(sensor_data));

// Read the Sensor Data
if (c.read(fd, &sensor_data, len) >= len) {

    // Convert the Sensor Data to Fixed-Point Numbers
    const humidity = floatToFixed(sensor_data.humidity);

    // Print the Sensor Data
    debug("humidity:{}.{:0>2}", .{
        humidity.int, 
        humidity.frac 
    });
}

(Source)

Here's the Humidity read from the BME280 Humidity Sensor...

nsh> sensortest test2
Zig Sensor Test
test_sensor2
humidity:78.81

Zig Generics

With Zig Generics and comptime, we can greatly simplify the reading of Sensor Data...

// Read the Temperature
const temperature: f32 = try sen.readSensor(
    c.struct_sensor_baro,       // Sensor Data Struct to be read
    "temperature",              // Sensor Data Field to be returned
    "/dev/uorb/sensor_baro0"  // Path of Sensor Device
);

// Print the Temperature
debug("temperature={}", .{ floatToFixed(temperature) });

(Source)

Here's the implementation of readSensor...

/// Read a Sensor and return the Sensor Data
pub fn readSensor(
comptime SensorType: type, // Sensor Data Struct to be read, like c.struct_sensor_baro
comptime field_name: []const u8, // Sensor Data Field to be returned, like "temperature"
device_path: []const u8 // Path of Sensor Device, like "/dev/sensor/sensor_baro0"
) !f32 {
// Open the Sensor Device
const fd = c.open(
&device_path[0], // Path of Sensor Device
c.O_RDONLY | c.O_NONBLOCK // Open for read-only
);
// Check for error
if (fd < 0) {
std.log.err("Failed to open device:{s}", .{ c.strerror(errno()) });
return error.OpenError;
}
// Close the Sensor Device when this function returns
defer {
_ = c.close(fd);
}
// Set Standby Interval
const interval: c_uint = 1_000_000; // 1,000,000 microseconds (1 second)
var ret = c.ioctl(fd, c.SNIOC_SET_INTERVAL, interval);
// Check for error
if (ret < 0 and errno() != c.ENOTSUP) {
std.log.err("Failed to set interval:{s}", .{ c.strerror(errno()) });
return error.IntervalError;
}
// Set Batch Latency
const latency: c_uint = 0; // No latency
ret = c.ioctl(fd, c.SNIOC_BATCH, latency);
// Check for error
if (ret < 0 and errno() != c.ENOTSUP) {
std.log.err("Failed to batch:{s}", .{ c.strerror(errno()) });
return error.BatchError;
}
// Poll for Sensor Data
var fds = std.mem.zeroes(c.struct_pollfd);
fds.fd = fd;
fds.events = c.POLLIN;
ret = c.poll(&fds, 1, -1);
// Check if Sensor Data is available
if (ret <= 0) {
std.log.err("Sensor data not available", .{});
return error.DataError;
}
// Define the Sensor Data Type
var sensor_data = std.mem.zeroes(
SensorType
);
const len = @sizeOf(
@TypeOf(sensor_data)
);
// Read the Sensor Data
const read_len = c.read(fd, &sensor_data, len);
// Check size of Sensor Data
if (read_len < len) {
std.log.err("Sensor data incorrect size", .{});
return error.SizeError;
}
// Return the Sensor Data Field
return @field(sensor_data, field_name);
}

Note that the Sensor Data Struct Type and the Sensor Data Field are declared as comptime...

/// Read a Sensor and return the Sensor Data
pub fn readSensor(
    comptime SensorType: type,        // Sensor Data Struct to be read, like c.struct_sensor_baro
    comptime field_name: []const u8,  // Sensor Data Field to be returned, like "temperature"
    device_path: []const u8           // Path of Sensor Device, like "/dev/uorb/sensor_baro0"
) !f32 { ...

Which means that the values will be substituted at Compile-Time. (Works like a C Macro)

We can then refer to the Sensor Data Struct sensor_baro like this...

    // Define the Sensor Data Type
    var sensor_data = std.mem.zeroes(
        SensorType
    );

And return a field temperature like this...

    // Return the Sensor Data Field
    return @field(sensor_data, field_name);

Thus this program...

// Read the Temperature
const temperature: f32 = try sen.read_sensor(
c.struct_sensor_baro, // Sensor Data Struct to be read
"temperature", // Sensor Data Field to be returned
"/dev/sensor/sensor_baro0" // Path of Sensor Device
);
// Print the Temperature
debug("temperature={}", .{
floatToFixed(temperature)
});
// Read the Pressure
const pressure: f32 = try sen.read_sensor(
c.struct_sensor_baro, // Sensor Data Struct to be read
"pressure", // Sensor Data Field to be returned
"/dev/sensor/sensor_baro0" // Path of Sensor Device
);
// Print the Pressure
debug("pressure={}", .{
floatToFixed(pressure)
});
// Read the Humidity
const humidity: f32 = try sen.read_sensor(
c.struct_sensor_humi, // Sensor Data Struct to be read
"humidity", // Sensor Data Field to be returned
"/dev/sensor/sensor_humi0" // Path of Sensor Device
);
// Print the Humidity
debug("humidity={}", .{
floatToFixed(humidity)
});

Produces this output...

NuttShell (NSH) NuttX-10.3.0
nsh> sensortest visual
Zig Sensor Test
Start main
...
temperature=30.18
pressure=1007.69
humidity=68.67
End main

CBOR Encoding

Blockly will emit the Zig code below for a typical IoT Sensor App: visual.zig

// Read Temperature from BME280 Sensor
const temperature = try sen.readSensor(  // Read BME280 Sensor
    c.struct_sensor_baro,       // Sensor Data Struct
    "temperature",              // Sensor Data Field
    "/dev/uorb/sensor_baro0"  // Path of Sensor Device
);

// Read Pressure from BME280 Sensor
const pressure = try sen.readSensor(  // Read BME280 Sensor
    c.struct_sensor_baro,       // Sensor Data Struct
    "pressure",                 // Sensor Data Field
    "/dev/uorb/sensor_baro0"  // Path of Sensor Device
);

// Read Humidity from BME280 Sensor
const humidity = try sen.readSensor(  // Read BME280 Sensor
    c.struct_sensor_humi,       // Sensor Data Struct
    "humidity",                 // Sensor Data Field
    "/dev/uorb/sensor_humi0"  // Path of Sensor Device
);

// Compose CBOR Message with Temperature, Pressure and Humidity
const msg = try composeCbor(.{
    "t", temperature,
    "p", pressure,
    "h", humidity,
});

// Transmit message to LoRaWAN
try transmitLorawan(msg);

This reads the Temperature, Pressure and Humidity from BME280 Sensor, composes a CBOR Message that's encoded with the Sensor Data, and transmits the CBOR Message to LoRaWAN.

composeCbor will work for a variable number of arguments? Strings as well as numbers?

Yep, here's the implementation of composeCbor: visual.zig

/// TODO: Compose CBOR Message with Key-Value Pairs
/// https://lupyuen.github.io/articles/cbor2
fn composeCbor(args: anytype) !CborMessage {
    debug("composeCbor", .{});
    comptime {
        assert(args.len % 2 == 0);  // Missing Key or Value
    }

    // Process each field...
    comptime var i: usize = 0;
    var msg = CborMessage{};
    inline while (i < args.len) : (i += 2) {

        // Get the key and value
        const key   = args[i];
        const value = args[i + 1];

        // Print the key and value
        debug("  {s}: {}", .{
            @as([]const u8, key),
            floatToFixed(value)
        });

        // Format the message for testing
        var slice = std.fmt.bufPrint(
            msg.buf[msg.len..], 
            "{s}:{},",
            .{
                @as([]const u8, key),
                floatToFixed(value)
            }
        ) catch { _ = std.log.err("Error: buf too small", .{}); return error.Overflow; };
        msg.len += slice.len;
    }
    debug("  msg={s}", .{ msg.buf[0..msg.len] });
    return msg;
}

/// TODO: CBOR Message
/// https://lupyuen.github.io/articles/cbor2
const CborMessage = struct {
    buf: [256]u8 = undefined,  // Limit to 256 chars
    len: usize = 0,
};

Note that composeCbor is declared as anytype...

fn composeCbor(args: anytype) { ...

That's why composeCbor accepts a variable number of arguments with different types.

To handle each argument, this inline / comptime loop is unrolled at Compile-Time...

    // Process each field...
    comptime var i: usize = 0;
    inline while (i < args.len) : (i += 2) {

        // Get the key and value
        const key   = args[i];
        const value = args[i + 1];

        // Print the key and value
        debug("  {s}: {}", .{
            @as([]const u8, key),
            floatToFixed(value)
        });
        ...
    }

What happens if we omit a Key or a Value when calling composeCbor?

This comptime assertion check will fail at Compile-Time...

comptime {
    assert(args.len % 2 == 0);  // Missing Key or Value
}

Customise Blockly

Next we customise Blockly to generate the Zig Sensor App.

Read the article...

Test Visual Zig Sensor App

To test the Zig Sensor App generated by Blockly, let's build an IoT Sensor App that will read Temperature, Pressure and Humidity from BME280 Sensor, and transmit the values to LoRaWAN...

lupyuen3.github.io/blockly-zig-nuttx/demos/code

Complex Sensor App

The Blocks above will emit this Zig program...

/// Main Function
pub fn main() !void {

  // Every 10 seconds...
  while (true) {
    const temperature = try sen.readSensor(  // Read BME280 Sensor
      c.struct_sensor_baro,       // Sensor Data Struct
      "temperature",              // Sensor Data Field
      "/dev/uorb/sensor_baro0"  // Path of Sensor Device
    );
    debug("temperature={}", .{ temperature });

    const pressure = try sen.readSensor(  // Read BME280 Sensor
      c.struct_sensor_baro,       // Sensor Data Struct
      "pressure",                 // Sensor Data Field
      "/dev/uorb/sensor_baro0"  // Path of Sensor Device
    );
    debug("pressure={}", .{ pressure });

    const humidity = try sen.readSensor(  // Read BME280 Sensor
      c.struct_sensor_humi,       // Sensor Data Struct
      "humidity",                 // Sensor Data Field
      "/dev/uorb/sensor_humi0"  // Path of Sensor Device
    );
    debug("humidity={}", .{ humidity });

    const msg = try composeCbor(.{  // Compose CBOR Message
      "t", temperature,
      "p", pressure,
      "h", humidity,
    });

    // Transmit message to LoRaWAN
    try transmitLorawan(msg);

    // Wait 10 seconds
    _ = c.sleep(10);
  }
}

(composeCbor is explained here)

Copy the contents of the Main Function and paste here...

visual-zig-nuttx/visual.zig

The generated Zig code should correctly read the Temperature, Pressure and Humidity from BME280 Sensor, and transmit the values to LoRaWAN...

NuttShell (NSH) NuttX-10.3.0
nsh> sensortest visual
Zig Sensor Test
Start main

temperature=31.05
pressure=1007.44
humidity=71.49
composeCbor
  t: 31.05
  p: 1007.44
  h: 71.49
  msg=t:31.05,p:1007.44,h:71.49,
transmitLorawan
  msg=t:31.05,p:1007.44,h:71.49,

temperature=31.15
pressure=1007.40
humidity=70.86
composeCbor
  t: 31.15
  p: 1007.40
  h: 70.86
  msg=t:31.15,p:1007.40,h:70.86,
transmitLorawan
  msg=t:31.15,p:1007.40,h:70.86,

temperature=31.16
pressure=1007.45
humidity=70.42
composeCbor
  t: 31.16
  p: 1007.45
  h: 70.42
  msg=t:31.16,p:1007.45,h:70.42,
transmitLorawan
  msg=t:31.16,p:1007.45,h:70.42,

temperature=31.16
pressure=1007.47
humidity=70.39
composeCbor
  t: 31.16
  p: 1007.47
  h: 70.39
  msg=t:31.16,p:1007.47,h:70.39,
transmitLorawan
  msg=t:31.16,p:1007.47,h:70.39,

temperature=31.19
pressure=1007.45
humidity=70.35
composeCbor
  t: 31.19
  p: 1007.45
  h: 70.35
  msg=t:31.19,p:1007.45,h:70.35,
transmitLorawan
  msg=t:31.19,p:1007.45,h:70.35,

temperature=31.20
pressure=1007.42
humidity=70.65
composeCbor
  t: 31.20
  p: 1007.42
  h: 70.65
  msg=t:31.20,p:1007.42,h:70.65,
transmitLorawan
  msg=t:31.20,p:1007.42,h:70.65,

(Tested with NuttX and BME280 on BL602)

Test Stubs

To test the Zig program above on Linux / macOS / Windows (instead of NuttX), add the stubs below to simulate a NuttX Sensor...

/// Import Standard Library
const std = @import("std");

/// Main Function
pub fn main() !void {
    // TODO: Paste here the contents of Zig Main Function generated by Blockly
    ...
}

/// Aliases for Standard Library
const assert = std.debug.assert;
const debug  = std.log.debug;

///////////////////////////////////////////////////////////////////////////////
//  CBOR Encoding

/// TODO: Compose CBOR Message with Key-Value Pairs
/// https://lupyuen.github.io/articles/cbor2
fn composeCbor(args: anytype) !CborMessage {
    debug("composeCbor", .{});
    comptime {
        assert(args.len % 2 == 0);  // Missing Key or Value
    }

    // Process each field...
    comptime var i: usize = 0;
    var msg = CborMessage{};
    inline while (i < args.len) : (i += 2) {

        // Get the key and value
        const key   = args[i];
        const value = args[i + 1];

        // Print the key and value
        debug("  {s}: {}", .{
            @as([]const u8, key),
            floatToFixed(value)
        });

        // Format the message for testing
        var slice = std.fmt.bufPrint(
            msg.buf[msg.len..], 
            "{s}:{},",
            .{
                @as([]const u8, key),
                floatToFixed(value)
            }
        ) catch { _ = std.log.err("Error: buf too small", .{}); return error.Overflow; };
        msg.len += slice.len;
    }
    debug("  msg={s}", .{ msg.buf[0..msg.len] });
    return msg;
}

/// TODO: CBOR Message
/// https://lupyuen.github.io/articles/cbor2
const CborMessage = struct {
    buf: [256]u8 = undefined,  // Limit to 256 chars
    len: usize = 0,
};

///////////////////////////////////////////////////////////////////////////////
//  Transmit To LoRaWAN

/// TODO: Transmit message to LoRaWAN
fn transmitLorawan(msg: CborMessage) !void { 
    debug("transmitLorawan", .{});
    debug("  msg={s}", .{ msg.buf[0..msg.len] });
}

///////////////////////////////////////////////////////////////////////////////
//  Stubs

const c = struct {
    const struct_sensor_baro = struct{};
    const struct_sensor_humi = struct{};
    fn sleep(seconds: c_uint) c_int {
        std.time.sleep(@as(u64, seconds) * std.time.ns_per_s);
        return 0;
    }
};

const sen = struct {
    fn readSensor(comptime SensorType: type, comptime field_name: []const u8, device_path: []const u8) !f32
        { _ = SensorType; _ = field_name; _ = device_path; return 23.45; }
};

fn floatToFixed(f: f32) f32 { return f; }

(composeCbor is explained here)

Debug Logger Crashes

UPDATE: This crashing seems to be caused by our Zig Sensor App running out of Stack Space. We fixed iy by increasing "Sensor Driver Test Stack Size" from 2048 to 4096.

Calling the debug logger inside print_valf2 causes weird crashes...

debug("timestamp: {}", .{ event.*.timestamp });

Possibly due to Memory Corruption inside our Zig Debug Logger...

/// Called by Zig for `std.log.debug`, `std.log.info`, `std.log.err`, ...
/// TODO: Support multiple threads
/// https://gist.github.com/leecannon/d6f5d7e5af5881c466161270347ce84d
pub fn log(...) void {

    // Possible memory corruption here: Format the message
    var slice = std.fmt.bufPrint(&log_buf, format, args)
        catch { _ = puts("*** Error: log_buf too small"); return; };
    ...

Here's a crash log...

bme280_fetch: temperature=31.570000 °C, pressure=1025.396118 mbar, humidity=64.624023 %
name: baro0
timestamp: 27270000
value1: 1025
value2: 31
size: 16
SensorTest: Received message: �, number:1/1
decode_insn_compressed: Compressed: a783
riscv_exception: EXCEPTION: Load access fault. MCAUSE: 00000005
riscv_exception: PANIC!!! Exception = 00000005
up_assert: Assertion failed at file:common/riscv_exception.c line: 89 task: sensortest
backtrace| 3: 0x2300c698
riscv_registerdump: EPC: 2300c698
riscv_registerdump: A0: 4201b9a0 A1: 0000a80 A2: 4201bf48 A3: 00000000
riscv_registerdump: A4: 2307a5e8 A5: 00583000 A6: 2307a000 A7: 00000000
riscv_registerdump: T0: 000001ff T1: 23005830 T2: 0000002d T3: 00000068
riscv_registerdump: T4: 00000009 T5: 0000002a T6: 0000002e
riscv_registerdump: S0: 4201b9a0 S1: 2307a000 S2: 00000a80 S3: 4201bdef
riscv_registerdump: S4: 00000000 S5: 00000000 S6: 00000000 S7: 00000000
riscv_registerdump: S8: 00000000 S9: 00000000 S10: 00000000 S11: 00000000
riscv_registerdump: SP: 4201bef0 FP: 4201b9a0 TP: 00000000 RA: 2300c78e

Another crash...

up_assert: Assertion failed at file:common/riscv_doirq.c line: 78 task: sensortest
backtrace| 3: 0x2300bd9a
riscv_registerdump: EPC: 2300bd9a
riscv_registerdump: A0: 00000000 A1: 4201bc38 A2: 00000013 A3: 00000000
riscv_registerdump: A4: 00000000 A5: 0000000b A6: a0000000 A7: 2306a1b2
riscv_registerdump: T0: f0000000 T1: 80000000 T2: 00000000 T3: 00000000
riscv_registerdump: T4: 00000008 T5: 00010000 T6: 6d0cb600
riscv_registerdump: S0: 0000001b S1: 00000000 S2: 23079000 S3: 4201b8c8
riscv_registerdump: S4: 4201bc38 S5: 00000006 S6: 00000000 S7: 00000000
riscv_registerdump: S8: 00000000 S9: 00000000 S10: 00000000 S11: 00000000
riscv_registerdump: SP: 4201bbf0 FP: 0000001b TP: 00000000 RA: 2300bd78

Which happens when closing a file (console?)...

/home/user/nuttx/nuttx/fs/inode/fs_files.c:380
  /* if f_inode is NULL, fd was closed */
  if (!(*filep)->f_inode)
2300bd98:	441c                	lw	a5,8(s0)
2300bd9a:	c39d                	beqz	a5,2300bdc0 <fs_getfilep+0xaa>
2300bd9c:	87a2                	mv	a5,s0
2300bd9e:	00fa2023          	sw	a5,0(s4)

And another crash...

up_assert: Assertion failed at file:mm_heap/mm_free.c line: 154 task: sensortest
riscv_registerdump: EPC: 230086b0
riscv_registerdump: A0: 00000000 A1: 4201bc38 A2: 00000000 A3: 00000000
riscv_registerdump: A4: 23078460 A5: 23078000 A6: 4201bdbc A7: 23078000
riscv_registerdump: T0: 000001ff T1: 23078460 T2: 0000002d T3: 00000068
riscv_registerdump: T4: 00000009 T5: 0000002a T6: 0000002e
riscv_registerdump: S0: 4201c1f0 S1: 23078000 S2: 4201b800 S3: 4201bed0
riscv_registerdump: S4: 42013000 S5: 23078000 S6: 00000000 S7: 00000000
riscv_registerdump: S8: 00000081 S9: 00000025 S10: 23068e25 S11: 4201bec4
riscv_registerdump: SP: 4201beb0 FP: 00000000 TP: 23001478 RA: 230080c2

This crashes inside free when deallocating the Sensor Data Buffer, might be due to a Heap Problem.

For safety, we converted the Heap Buffer to a Static Buffer.