Skip to content

Commit 5acc06c

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat: real-time sensor data streaming via looper + event queue
Replaces the static sensor info polling with actual event streaming: - CreateEventQueue, EnableSensor, SetEventRate, HasEvents added to idiomatic sensor package via overlay - sensor read command now creates a looper, event queue, enables the sensor, and reads ASensorEvent structs in real-time - Verified on Pixel 8a: accelerometer (31 events/2s at 100ms rate), gyroscope (20 events/1s at 50ms rate) Example: ndkcli sensor read --type 1 --duration 5s --rate 50000 Output: [0] t=7362583743842 x=1.303490 y=9.522836 z=2.161317
1 parent 589c656 commit 5acc06c

6 files changed

Lines changed: 161 additions & 47 deletions

File tree

cmd/ndkcli/sensor_workflow.go

Lines changed: 96 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,121 @@ package main
33
import (
44
"fmt"
55
"time"
6+
"unsafe"
67

78
"github.com/spf13/cobra"
9+
"github.com/xaionaro-go/ndk/looper"
810
"github.com/xaionaro-go/ndk/sensor"
911
)
1012

13+
// sensorEventData extracts x, y, z from an ASensorEvent.
14+
// ASensorEvent layout: version(4) + sensor(4) + type(4) + reserved(4) + timestamp(8) + data[16]float32
15+
// data offset = 24 bytes from the start.
16+
func sensorEventData(event *sensor.SensorEvent) (x, y, z float32, timestamp int64) {
17+
base := (*byte)(event.Pointer())
18+
// timestamp at offset 16 (after version+sensor+type+reserved = 4*4 = 16)
19+
timestamp = *(*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(base)) + 16))
20+
// data[0..2] at offset 24
21+
dataPtr := uintptr(unsafe.Pointer(base)) + 24
22+
x = *(*float32)(unsafe.Pointer(dataPtr))
23+
y = *(*float32)(unsafe.Pointer(dataPtr + 4))
24+
z = *(*float32)(unsafe.Pointer(dataPtr + 8))
25+
return
26+
}
27+
1128
var sensorReadCmd = &cobra.Command{
1229
Use: "read",
13-
Short: "Read sensor information and poll default sensor",
14-
Long: `Queries the sensor manager for the default sensor of the given type,
15-
prints its static properties, and optionally polls for the specified duration.
16-
17-
Note: Real-time sensor data streaming requires looper and event queue
18-
integration, which is beyond what a simple CLI poll can provide.
19-
This command demonstrates the sensor query API.`,
30+
Short: "Read real-time sensor data",
31+
Long: `Reads real-time sensor events using a looper and event queue.
32+
Prints x, y, z values for each event at the configured sample rate.`,
2033
RunE: func(cmd *cobra.Command, args []string) (_err error) {
2134
sensorType, _ := cmd.Flags().GetInt32("type")
2235
duration, _ := cmd.Flags().GetDuration("duration")
36+
rate, _ := cmd.Flags().GetInt32("rate")
2337

24-
mgr := sensor.GetInstance()
25-
26-
s := mgr.DefaultSensor(sensorType)
27-
if s.Pointer() == nil {
28-
return fmt.Errorf("no default sensor found for type %d", sensorType)
38+
if duration == 0 {
39+
duration = 5 * time.Second
2940
}
3041

31-
fmt.Printf("sensor info:\n")
32-
fmt.Printf(" name: %s\n", s.Name())
33-
fmt.Printf(" vendor: %s\n", s.Vendor())
34-
fmt.Printf(" type: %d (%s)\n", s.Type(), sensor.Type(s.Type()))
35-
fmt.Printf(" resolution: %g\n", s.Resolution())
36-
fmt.Printf(" min delay: %d us\n", s.MinDelay())
37-
38-
fmt.Println("\nnote: real-time sensor data streaming requires looper + event queue integration")
39-
40-
if duration > 0 {
41-
fmt.Printf("\npolling sensor info for %v...\n", duration)
42-
deadline := time.Now().Add(duration)
43-
tick := 0
44-
for time.Now().Before(deadline) {
45-
polled := mgr.DefaultSensor(sensorType)
46-
if polled.Pointer() == nil {
47-
fmt.Printf(" [%d] sensor no longer available\n", tick)
48-
} else {
49-
fmt.Printf(" [%d] %s (type=%d, min_delay=%d us)\n",
50-
tick, polled.Name(), polled.Type(), polled.MinDelay())
51-
}
52-
tick++
53-
time.Sleep(time.Second)
42+
looper.Run(func(lp *looper.Looper) {
43+
_err = readSensorEvents(lp, sensorType, duration, rate)
44+
})
45+
return
46+
},
47+
}
48+
49+
func readSensorEvents(
50+
lp *looper.Looper,
51+
sensorType int32,
52+
duration time.Duration,
53+
rateUs int32,
54+
) error {
55+
mgr := sensor.GetInstance()
56+
57+
s := mgr.DefaultSensor(sensorType)
58+
if s.Pointer() == nil {
59+
return fmt.Errorf("no default sensor found for type %d", sensorType)
60+
}
61+
62+
fmt.Printf("sensor: %s (%s)\n", s.Name(), sensor.Type(s.Type()))
63+
fmt.Printf("vendor: %s\n", s.Vendor())
64+
fmt.Printf("resolution: %g, min delay: %d us\n", s.Resolution(), s.MinDelay())
65+
66+
// Create event queue on the current looper thread.
67+
const looperIdent = 1
68+
queue := mgr.CreateEventQueue(
69+
(*sensor.ALooper)(lp.Pointer()),
70+
looperIdent,
71+
sensor.ALooper_callbackFunc(nil), // no callback — we poll manually
72+
nil,
73+
)
74+
if queue == nil || queue.Pointer() == nil {
75+
return fmt.Errorf("failed to create sensor event queue")
76+
}
77+
defer mgr.DestroyEventQueue(queue)
78+
79+
// Enable the sensor on the queue and set the sample rate.
80+
if err := queue.EnableSensor(s); err != nil {
81+
return fmt.Errorf("enabling sensor: %w", err)
82+
}
83+
if err := queue.SetEventRate(s, rateUs); err != nil {
84+
return fmt.Errorf("setting event rate: %w", err)
85+
}
86+
87+
fmt.Printf("\nstreaming for %v (rate=%d us)...\n", duration, rateUs)
88+
89+
deadline := time.Now().Add(duration)
90+
count := 0
91+
// Allocate a raw ASensorEvent buffer (104 bytes per event on arm64).
92+
// SensorEvent.ptr must point to valid C memory for GetEvents to write into.
93+
const eventSize = 104 // sizeof(ASensorEvent)
94+
eventBuf := make([]byte, eventSize)
95+
event := sensor.NewSensorEventFromPointer(unsafe.Pointer(&eventBuf[0]))
96+
97+
for time.Now().Before(deadline) {
98+
// Poll the looper for events (100ms timeout).
99+
looper.PollOnce(100*time.Millisecond, nil, nil, nil)
100+
101+
// Drain all available events.
102+
for {
103+
n := queue.GetEvents(event, 1)
104+
if n <= 0 {
105+
break
54106
}
55-
fmt.Printf("polling complete (%d samples)\n", tick)
107+
x, y, z, ts := sensorEventData(event)
108+
fmt.Printf(" [%d] t=%d x=%.6f y=%.6f z=%.6f\n", count, ts, x, y, z)
109+
count++
56110
}
111+
}
57112

58-
return nil
59-
},
113+
fmt.Printf("received %d events\n", count)
114+
return nil
60115
}
61116

62117
func init() {
63-
sensorReadCmd.Flags().Int32("type", int32(sensor.Accelerometer), "sensor type ID (1=accelerometer, 2=magnetic, 4=gyroscope, 5=light, ...)")
64-
sensorReadCmd.Flags().Duration("duration", 0, "how long to poll (0 = just print info)")
118+
sensorReadCmd.Flags().Int32("type", int32(sensor.Accelerometer), "sensor type ID (1=accelerometer, 2=magnetic, 3=orientation, 4=gyroscope, 5=light, 8=proximity)")
119+
sensorReadCmd.Flags().Duration("duration", 5*time.Second, "how long to stream")
120+
sensorReadCmd.Flags().Int32("rate", 100000, "sample period in microseconds (default 100ms)")
65121

66122
sensorCmd.AddCommand(sensorReadCmd)
67123
}

sensor/a_looper.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sensor/a_looper_callback_func.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sensor/event_queue.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sensor/manager.go

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/overlays/sensor.yaml

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,30 @@ functions:
4444

4545
# Manager — event queue lifecycle
4646
ASensorManager_createEventQueue:
47-
skip: true # needs unsafe.Pointer params (looper, callback, data)
47+
receiver: Manager
48+
go_name: CreateEventQueue
49+
returns_new: EventQueue
4850

4951
ASensorManager_destroyEventQueue:
50-
skip: true # needs manager+queue params, can't be a simple method
52+
receiver: Manager
53+
go_name: DestroyEventQueue
5154

52-
# EventQueue operations (skipped: no way to create EventQueue in idiomatic API)
5355
ASensorEventQueue_enableSensor:
54-
skip: true
56+
receiver: EventQueue
57+
go_name: EnableSensor
58+
5559
ASensorEventQueue_disableSensor:
56-
skip: true
60+
receiver: EventQueue
61+
go_name: DisableSensor
62+
5763
ASensorEventQueue_setEventRate:
58-
skip: true
64+
receiver: EventQueue
65+
go_name: SetEventRate
66+
5967
ASensorEventQueue_hasEvents:
60-
skip: true
68+
receiver: EventQueue
69+
go_name: HasEvents
70+
pure: true
6171

6272
# Sensor info getters
6373
ASensor_getName:

0 commit comments

Comments
 (0)