-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathboard-simulator.go
493 lines (431 loc) · 13.9 KB
/
board-simulator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
//go:build !baremetal
package board
// The generic board exists for testing locally without running on real
// hardware. This avoids potentially long edit-flash-test cycles.
import (
"bufio"
"errors"
"fmt"
"io"
"math/rand"
"os"
"os/exec"
"strings"
"sync"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/pixel"
)
const (
// The board name, as passed to TinyGo in the "-target" flag.
// This is the special name "simulator" for the simulator.
Name = "simulator"
)
// List of all devices.
//
// Support varies by board, but all boards have the following peripherals
// defined.
var (
Power = simulatedPower{}
Sensors = &simulatedSensors{}
Display = mainDisplay{}
Buttons = buttonsConfig{}
)
func init() {
AddressableLEDs = &simulatedLEDs{}
}
type simulatedPower struct{}
// Configure the battery status reader. This must be called before calling
// Status.
func (p simulatedPower) Configure() {
// Nothing to do here.
}
// Status returns the current charge status (charging, discharging) and the
// current voltage of the battery in microvolts. If the voltage is 0 it means
// there is no battery present, any other value means a value was read from the
// battery (but there may or may not be a battery attached).
//
// The percent is a rough approximation of the state of charge of the battery.
// The value -1 means the state of charge is unknown.
// It is often inaccurate while charging. It may be best to just show "charging"
// instead of a specific percentage.
func (p simulatedPower) Status() (state ChargeState, microvolts uint32, percent int8) {
// Pretend we're running on battery power and the battery is at 3.7V
// (typical lipo voltage).
actualMicrovolts := uint32(3700_000)
// Randomize the output a bit to fake ADC noise (programs should be able to
// deal with that).
microvolts = actualMicrovolts + rand.Uint32()%16384 - 8192
// Use a stable percent though, otherwise BLE battery level notifications
// will fluctuate way too much.
percent = lithumBatteryApproximation.approximate(actualMicrovolts)
return Discharging, microvolts, percent
}
type mainDisplay struct{}
type fyneScreen struct {
width int
height int
keyevents []KeyEvent
keyeventsLock sync.Mutex
touchID uint32
touches [1]TouchPoint
touchesLock sync.Mutex
}
var screen = &fyneScreen{}
// Configure returns a new display ready to draw on.
//
// Boards without a display will return nil.
func (d mainDisplay) Configure() Displayer[pixel.RGB888] {
startWindow()
screen.width = Simulator.WindowWidth
screen.height = Simulator.WindowHeight
windowSendCommand(fmt.Sprintf("display %d %d", screen.width, screen.height), nil)
return screen
}
// MaxBrightness returns the maximum brightness value. A maximum brightness
// value of 0 means that this display doesn't support changing the brightness.
func (d mainDisplay) MaxBrightness() int {
return 1
}
// SetBrightness sets brightness level of the display. It should be:
//
// 0 ≤ level ≤ MaxBrightness
//
// A value of 0 turns the backlight off entirely (but may leave the display
// running with nothing visible).
func (d mainDisplay) SetBrightness(level int) {
// Send the current and max brightness levels.
windowSendCommand(fmt.Sprintf("display-brightness %d %d", level, 1), nil)
}
// Wait until the next vertical blanking interval (vblank) interrupt is
// received. If the vblank interrupt is not available, it waits until the time
// since the previous call to WaitForVBlank is the default interval instead.
//
// The vertical blanking interval is the time between two screen refreshes. The
// vblank interrupt happens at the start of this interval, and indicates the
// period where the framebuffer is not being touched and can be updated without
// tearing.
//
// Don't use this method for timing, because vblank varies by hardware. Instead,
// use time.Now() to determine the current time and the amount of time since the
// last screen refresh.
//
// TODO: this is not a great API (it's blocking), it may change in the future.
func (d mainDisplay) WaitForVBlank(defaultInterval time.Duration) {
// I'm sure there is some SDL2 API we could use here, but I couldn't find
// one easily so just emulate it.
dummyWaitForVBlank(defaultInterval)
}
// Pixels per inch for this display.
func (d mainDisplay) PPI() int {
return Simulator.WindowPPI
}
func (d mainDisplay) ConfigureTouch() TouchInput {
startWindow()
return sdltouch{}
}
func (s *fyneScreen) Display() error {
// Nothing to do here.
return nil
}
func (s *fyneScreen) DrawBitmap(x, y int16, image pixel.Image[pixel.RGB888]) error {
displayWidth, displayHeight := s.Size()
width, height := image.Size()
if x < 0 || y < 0 || width <= 0 || height <= 0 ||
int(x)+width > int(displayWidth) || int(y)+height > int(displayHeight) {
return errors.New("board: drawing out of bounds")
}
buf := image.RawBuffer()
drawStart := time.Now()
lastUpdate := drawStart
for bufy := 0; bufy < int(height); bufy++ {
// Delay drawing a bit, to simulate a slow SPI bus.
if Simulator.WindowDrawSpeed != 0 {
now := time.Now()
expected := drawStart.Add(Simulator.WindowDrawSpeed * time.Duration(bufy*int(width)))
delay := expected.Sub(now)
if delay > 0 {
time.Sleep(delay)
now = time.Now()
}
if now.Sub(lastUpdate) > 5*time.Millisecond {
lastUpdate = now
}
}
index := (bufy * int(width)) * 3
lineBuf := buf[index : index+int(width)*3]
windowSendCommand(fmt.Sprintf("draw %d %d %d", x, int(y)+bufy, width), lineBuf)
}
return nil
}
func (s *fyneScreen) Size() (width, height int16) {
return int16(s.width), int16(s.height)
}
// Set sleep mode for this screen.
func (s *fyneScreen) Sleep(sleepEnabled bool) error {
// This is a no-op.
// TODO: use a different gray than when the backlight is set to zero, to
// indicate sleep mode.
return nil
}
var errNoRotation = errors.New("error: SetRotation isn't supported")
func (s *fyneScreen) Rotation() drivers.Rotation {
return drivers.Rotation0
}
func (s *fyneScreen) SetRotation(rotation drivers.Rotation) error {
// TODO: implement this, to be able to test rotation support.
return errNoRotation
}
func (s *fyneScreen) SetScrollArea(topFixedArea, bottomFixedArea int16) {
windowSendCommand(fmt.Sprintf("scroll-start %d %d", topFixedArea, bottomFixedArea), nil)
}
func (s *fyneScreen) SetScroll(line int16) {
windowSendCommand(fmt.Sprintf("scroll %d", line), nil)
}
func (s *fyneScreen) StopScroll() {
windowSendCommand(fmt.Sprintf("scroll-stop"), nil)
}
type sdltouch struct{}
func (s sdltouch) ReadTouch() []TouchPoint {
screen.touchesLock.Lock()
defer screen.touchesLock.Unlock()
if screen.touches[0].ID != 0 {
return screen.touches[:1]
}
return nil
}
type buttonsConfig struct{}
func (b buttonsConfig) Configure() {
}
func (b buttonsConfig) ReadInput() {
}
func (b buttonsConfig) NextEvent() KeyEvent {
screen.keyeventsLock.Lock()
defer screen.keyeventsLock.Unlock()
if len(screen.keyevents) != 0 {
event := screen.keyevents[0]
copy(screen.keyevents, screen.keyevents[1:])
screen.keyevents = screen.keyevents[:len(screen.keyevents)-1]
return event
}
return NoKeyEvent
}
type simulatedSensors struct {
configured drivers.Measurement
lock sync.Mutex
accelSource [3]float64
stepsSource uint32
accel [3]int32
steps uint32
temp int32
}
// Configure configures all sensors as specified in the which parameter.
// If there is an error, none of the sensors can be relied upon to work.
func (s *simulatedSensors) Configure(which drivers.Measurement) error {
s.configured = which
return nil
}
// Update updates the sensor values as given in the which parameter.
// All sensors in the which parameter must have been configured before, or the
// behavior may be unpredictable.
func (s *simulatedSensors) Update(which drivers.Measurement) error {
if which != s.configured&which {
// This is a bug. Don't check it on each board, but do check it in the
// simulator.
panic("asked to update sensors that weren't configured")
}
if which&drivers.Acceleration != 0 {
s.lock.Lock()
// Add some noise to the accelerometer to make the values more
// realistic.
s.accel[0] = rand.Int31n(30_000) - 15_000 + int32(s.accelSource[0]*1000_000) // x
s.accel[1] = rand.Int31n(30_000) - 15_000 + int32(s.accelSource[1]*1000_000) // y
s.accel[2] = rand.Int31n(30_000) - 15_000 + int32(s.accelSource[2]*1000_000) // z
s.steps = s.stepsSource
s.lock.Unlock()
}
if which&drivers.Temperature != 0 {
// Temperature around 20°C (with some jitter thrown in for a good
// simulation).
s.temp = 20000 + rand.Int31n(200) - 100
}
return nil
}
// Acceleration returns the last read acceleration in µg (micro-gravity). This
// includes gravity: when one of the axes is pointing straight to Earth and the
// sensor is not moving the returned value will be around 1000000 or -1000000.
//
// The accelerometer values match those used on Android. When the device is
// lying flat on a table, the Z axis is around 1g. When the device is rotated
// 90° upright, the Y axis is around 1g. When the device is then rotated 90° to
// the left (counter-clockwise), the X axis is around 1g.
//
// The simulator returns values as if the device is held upright like you'd hold
// a phone while taking a selfie.
func (s *simulatedSensors) Acceleration() (x, y, z int32) {
return s.accel[0], s.accel[1], s.accel[2]
}
// Steps returns the number of steps since the step counter started.
// The uint32 value is assumed to be large enough for all practical use cases.
//
// The value can be incremented from the simulator.
func (s *simulatedSensors) Steps() (steps uint32) {
return s.steps
}
// Temperature returns the temperature that was last read from the sensor.
// If there are multiple temperature sensors on a given board, the most accurate
// result will be returned.
//
// The simulator returns a fixed temperature, with some jitter to make it look
// more like a real-world sensor (no sensor is without noise).
func (s *simulatedSensors) Temperature() int32 {
return s.temp
}
type simulatedLEDs struct {
data []byte
}
// Initialize the addressable LEDs.
//
// The way to determine whether there are addressable LEDs on a given board, is
// to configure them and then check the length of board.AddressableLEDs.Data.
func (l *simulatedLEDs) Configure() {
startWindow()
l.data = make([]byte, Simulator.AddressableLEDs*3)
l.Update()
}
func (l *simulatedLEDs) Len() int {
return len(l.data) / 3
}
func (l *simulatedLEDs) SetRGB(i int, r, g, b uint8) {
l.data[i*3+0] = r
l.data[i*3+1] = g
l.data[i*3+2] = b
}
// Update the LEDs with the color data.
func (l *simulatedLEDs) Update() {
cmd := fmt.Sprintf("addressable-leds %d", l.Len())
windowSendCommand(cmd, l.data)
}
var (
fyneStart sync.Once
windowLock sync.Mutex
windowStdin io.WriteCloser
windowStdout io.ReadCloser
)
// Ensure the window is running in a separate process, starting it if necessary.
func startWindow() {
// Create a main loop for Fyne.
windowRunning := make(chan struct{})
fyneStart.Do(func() {
// Start the separate process that manages the window.
go func() {
cmd := exec.Command(os.Args[0], runWindowCommand)
cmd.Stderr = os.Stderr
windowStdin, _ = cmd.StdinPipe()
windowStdout, _ = cmd.StdoutPipe()
err := cmd.Start()
if err != nil {
fmt.Fprintln(os.Stdout, "could not start window process:", err)
os.Exit(1)
}
close(windowRunning)
err = cmd.Wait()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
os.Exit(1)
}
// The window was closed, so exit.
os.Exit(0)
}()
<-windowRunning
// Listen for events (keyboard/touch).
go windowListenEvents()
// Do some initialization.
windowSendCommand("title "+Simulator.WindowTitle, nil)
})
}
// Send a command to the separate process that manages the window.
// The command is a single line (without newline). The data part is optional
// binary data that can be sent with the command. The size of this binary data
// must be part of the textual command.
func windowSendCommand(command string, data []byte) {
windowLock.Lock()
defer windowLock.Unlock()
windowStdin.Write([]byte(command + "\n"))
windowStdin.Write(data)
}
// Goroutine that listens for window events like button and touch (keyboard and
// mouse).
func windowListenEvents() {
r := bufio.NewReader(windowStdout)
for {
line, err := r.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
fmt.Fprintln(os.Stderr, "failed to read I/O events from child process:", err)
}
cmd := strings.Fields(line)[0]
switch cmd {
case "keypress", "keyrelease":
// Read the key code.
var key KeyEvent
fmt.Sscanf(line, "%s %d", &cmd, &key)
if cmd == "keyrelease" {
key |= keyReleased
}
// Add the key code to the
screen.keyeventsLock.Lock()
screen.keyevents = append(screen.keyevents, key)
screen.keyeventsLock.Unlock()
case "mousedown":
// Read the event.
var x, y int16
fmt.Sscanf(line, "%s %d %d", &cmd, &x, &y)
// Update the touch state.
screen.touchesLock.Lock()
screen.touchID++
screen.touches[0] = TouchPoint{
ID: screen.touchID,
X: x,
Y: y,
}
screen.touchesLock.Unlock()
case "mouseup":
// End the current touch.
screen.touchesLock.Lock()
screen.touches[0] = TouchPoint{} // no active touch
screen.touchesLock.Unlock()
case "mousemove":
// Read the event.
var x, y int16
fmt.Sscanf(line, "%s %d %d", &cmd, &x, &y)
// Update the touch state.
screen.touchesLock.Lock()
if screen.touches[0].ID != 0 {
screen.touches[0].X = x
screen.touches[0].Y = y
}
screen.touchesLock.Unlock()
case "accel":
var x, y, z float64
fmt.Sscanf(line, "%s %f %f %f", &cmd, &x, &y, &z)
Sensors.lock.Lock()
Sensors.accelSource[0] = x
Sensors.accelSource[1] = y
Sensors.accelSource[2] = z
Sensors.lock.Unlock()
case "steps":
var n uint32
fmt.Sscanf(line, "%s %d %d", &cmd, &n)
Sensors.lock.Lock()
Sensors.stepsSource = n
Sensors.lock.Unlock()
default:
fmt.Fprintln(os.Stderr, "unknown command:", cmd)
}
}
}