mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46bde26249 | |||
| 0bc660e1bc | |||
| dc6edbb694 | |||
| 1d695a231a | |||
| 04acd8e666 | |||
| bb2d365868 | |||
| f459992f3c | |||
| 62663c1832 | |||
| 8f372935ac | |||
| fb3062433a | |||
| 9fc1c0aedc | |||
| 1c10dea443 | |||
| e960a6ff57 | |||
| 2673cc1e9a | |||
| 0034fc511a | |||
| a0c5da601f | |||
| 21a7d0a96a | |||
| e232a4f136 | |||
| 4071028e85 | |||
| a514169c37 | |||
| b480978e1a | |||
| 9eb95a4651 | |||
| d6114c9f6d | |||
| 0e2fb829ef | |||
| 892265b733 | |||
| b390e3225a | |||
| 1513808425 | |||
| 0a41786a77 | |||
| c21cd39813 | |||
| a35786be70 | |||
| 2a42fa7cbb | |||
| 5d96a56603 | |||
| f931ad44fb | |||
| 50778af656 | |||
| 936a255df9 | |||
| 9ed648f4a5 | |||
| fab688a701 | |||
| d4654668f2 | |||
| 253f8e3220 | |||
| c710cc8236 | |||
| 4b831af52b | |||
| 09fd01340b | |||
| 23833e69c7 | |||
| 744fda5eec | |||
| 027c91272e | |||
| 5847506ba6 | |||
| e35e6b8e13 | |||
| 408851a9f5 | |||
| bd88b70511 | |||
| 34da2d208a | |||
| 5cb360a4bf | |||
| 51b604ce97 | |||
| ec680be784 | |||
| 5fb935001e | |||
| 297ad416d3 | |||
| 3fa08112db | |||
| b639f7b12e | |||
| 28d625abfd | |||
| 228e57cf98 | |||
| 6cf1eb86e5 | |||
| 857ab80ae6 | |||
| a31ba26a6c | |||
| 303ec94529 | |||
| 833990f44d | |||
| 28d87eb0c5 | |||
| ae9e8f915e | |||
| 45fad80c3e | |||
| 0304d30b78 | |||
| 7de0a0814e | |||
| 80356fd9d9 | |||
| c4ff8242a7 | |||
| 82c41dbf14 | |||
| c4168864fd | |||
| dbc9022f6a | |||
| d41bc0b85f | |||
| e7f90166ad | |||
| 156d6e7c9c | |||
| 62f51445b6 | |||
| 37ae0ad5b8 | |||
| 8de5ab7c64 | |||
| e14fbf6d3d | |||
| 4d0d8e9c14 | |||
| 964364005d | |||
| 0d80962dc8 | |||
| cb2ca239f0 | |||
| c5ff13ad23 | |||
| e6907db19e | |||
| 45e207fe2e | |||
| 677f8ed297 | |||
| b31c5ca9c9 | |||
| 1b726ef2bd | |||
| 7db9e9d6db | |||
| 845bd6fe93 | |||
| 97ef4986ba | |||
| d5db138d9a | |||
| ace4a8924b | |||
| 66da4422dc | |||
| 222f368681 | |||
| 3d491553dd | |||
| 46cd56951c | |||
| 2f85c8bd04 | |||
| 5b6571350d |
@@ -1,2 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
open_collective: tinygo
|
||||
|
||||
@@ -11,13 +11,12 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/tinygo-org/tinygo-dev:latest
|
||||
container:
|
||||
image: ghcr.io/tinygo-org/tinygo:latest
|
||||
options: --user root
|
||||
steps:
|
||||
- name: Work around CVE-2022-24765
|
||||
# We're not on a multi-user machine, so this is safe.
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
- name: TinyGo version check
|
||||
run: tinygo version
|
||||
- name: Enforce Go Formatted Code
|
||||
@@ -25,4 +24,6 @@ jobs:
|
||||
- name: Run unit tests
|
||||
run: make unit-test
|
||||
- name: Run build and smoke tests
|
||||
run: make smoke-test
|
||||
run: |
|
||||
go env -w GOFLAGS=-buildvcs=false
|
||||
make smoke-test
|
||||
|
||||
+142
@@ -1,3 +1,145 @@
|
||||
0.35.0
|
||||
---
|
||||
- **new devices**
|
||||
- **unoqmatrix**
|
||||
- LED matrix on the Arduino Uno Q
|
||||
- **waveshare-epd (ssd1680)**
|
||||
- Add driver for Waveshare 2.9 inch v2 e-paper display
|
||||
|
||||
- **enhancements**
|
||||
- **gps**
|
||||
- add UBX config command support (#831)
|
||||
- improve implementation for UBX config commands
|
||||
- revamp validSentence() to avoid heap allocation for errors
|
||||
- export some errors for checking/suppression from client
|
||||
- improvements and corrections for config commands
|
||||
- **lora**
|
||||
- fill out more constants for lora device
|
||||
- **mcp2515**
|
||||
- add support for extended CAN IDs (#857)
|
||||
- **si5351**
|
||||
- complete refactor for more complete interface
|
||||
- **st7735**
|
||||
- remove dependency on the machine package
|
||||
- **sx127x**
|
||||
- add functions used for FSK radio communication
|
||||
- **ws2812**
|
||||
- add brightness control
|
||||
- add PIO support for RP2040/RP2350
|
||||
|
||||
- **bugfixes**
|
||||
- **st7789**
|
||||
- fix scroll on rotated displays
|
||||
- fix driver when rotated 90º
|
||||
- **ws2812**
|
||||
- fix brightness control issues (#858)
|
||||
|
||||
|
||||
0.34.0
|
||||
---
|
||||
- **core**
|
||||
- add regmap package to facilitate heapless driver development
|
||||
- PinInput+PinOutput HAL (#753, reloaded) (#795)
|
||||
- Add Device8I2C/SPI types and their logic (#801)
|
||||
|
||||
- **new devices**
|
||||
- **bno8x**
|
||||
- Add support for CEVA BNO08x 9DoF sensor (#809)
|
||||
- **hineyhsc**
|
||||
- Add Honeywell HSC TruStability SPI+I2C pressure sensor driver (#799)
|
||||
- **p25q16h**
|
||||
- added support for P25Q16H flash chip for xiao-ble target
|
||||
- **si5351**
|
||||
- add support for si5351 (#810)
|
||||
- **w25q80dv**
|
||||
- added support for W25Q80DV flash chip for xiao-ble target
|
||||
- **w5500**
|
||||
- initial version the driver (#788)
|
||||
|
||||
- **enhancements**
|
||||
- **ds3231**
|
||||
- DS3231 Alarm features (#805)
|
||||
- **general**
|
||||
- add simplest driver ports
|
||||
- **lis3dh**
|
||||
- add Update and Acceleration calls
|
||||
- use correct error handling and make configurable
|
||||
- **lsm9ds1**
|
||||
- avoid unnecessary heap allocations
|
||||
- **pixel**
|
||||
- add Grayscale2bit color (#817)
|
||||
- **scd4x**
|
||||
- add support for SCD41 single-shot measurements
|
||||
- remove dead code
|
||||
- update package to use standard methods
|
||||
- **si5351**
|
||||
- add many missing functions needed for convenient use.
|
||||
- **ssd1xxx**
|
||||
- break dependency from machine package (#812)
|
||||
- **test**
|
||||
- Add TestImageRGB888 and TestImageRGB555
|
||||
|
||||
- **bugfixes**
|
||||
- **quadrature**
|
||||
- add RP2350 to quadrature_interrupt.go
|
||||
- **pixel**
|
||||
- correct logic error in image size checks in pixel's tests
|
||||
- correct logic error in image size checks in pixel's tests (Monochrome)
|
||||
- correct RGB555 to RGBA conversion logic
|
||||
|
||||
|
||||
0.33.0
|
||||
---
|
||||
- **new devices**
|
||||
- **ens160**
|
||||
- Add ens160 i2c driver
|
||||
- **lsm303dlhc**
|
||||
- added support for LSM303DLHC e-Compass; (#783)
|
||||
- **seesaw**
|
||||
- add support for Adafruit Seesaw encoders
|
||||
|
||||
- **enhancements**
|
||||
- **ws2812**
|
||||
- add RP2350 support
|
||||
- **ssd1306**
|
||||
- avoid unnecessary heap allocations (#767)
|
||||
- **gps**
|
||||
- allow gps init with address
|
||||
- **lsm6ds3tr**
|
||||
- avoid unnecessary heap allocations (#766)
|
||||
|
||||
- **bugfixes**
|
||||
- **gps**
|
||||
- Fix gps time calculation (#785)
|
||||
|
||||
|
||||
0.32.0
|
||||
---
|
||||
- **enhancements**
|
||||
- **bmp280**
|
||||
- remove alloc on read sensor data
|
||||
- **ws2812**
|
||||
- add 200MHz support for the Cortex-M0/rp2040
|
||||
|
||||
- **bugfixes**
|
||||
- **ssd1306**
|
||||
- remove time.Sleep from SSD1306 SPI transfer code
|
||||
- **tmc2209**
|
||||
- tmc2209 bug fixes (#755)
|
||||
|
||||
- **docs**
|
||||
- **contributing**
|
||||
- add driver design pointer to CONTRIBUTING.md
|
||||
|
||||
|
||||
0.31.0
|
||||
---
|
||||
---
|
||||
- **enhancements**
|
||||
- **spi**
|
||||
- update all SPI usage to use either *machine.SPI or drivers.SPI
|
||||
|
||||
|
||||
0.30.0
|
||||
---
|
||||
- **new devices**
|
||||
|
||||
@@ -8,6 +8,9 @@ We would like your help to make this project better, so we appreciate any contri
|
||||
|
||||
We'd love to get your feedback on getting started with TinyGo. Run into any difficulty, confusion, or anything else? You are not alone. We want to know about your experience, so we can help the next people. Please open a Github issue with your questions, or you can also get in touch directly with us on our Slack channel at [https://gophers.slack.com/messages/CDJD3SUP6](https://gophers.slack.com/messages/CDJD3SUP6).
|
||||
|
||||
### Driver design
|
||||
Before porting or writing a driver from scratch please read **[Driver Design for TinyGo](https://tinygo.org/docs/guides/driver-design)**.
|
||||
|
||||
### One of the TinyGo drivers is not working as you expect
|
||||
|
||||
Please open a Github issue with your problem, and we will be happy to assist.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved.
|
||||
Copyright The TinyGo Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
||||
@@ -26,3 +26,17 @@ unit-test:
|
||||
@go test -v $(addprefix ./,$(TESTS))
|
||||
|
||||
test: clean fmt-check unit-test smoke-test
|
||||
|
||||
EXCLUDE_DIRS = build cmd examples internal lora ndir netdev netlink tester
|
||||
|
||||
drivers-count:
|
||||
@root_count=$$(find . -mindepth 1 -maxdepth 1 -type d | grep -vE '^\./($(subst $(space),|,$(EXCLUDE_DIRS)))$$' | wc -l); \
|
||||
epd_count=$$(find ./waveshare-epd -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l); \
|
||||
total=$$((root_count + epd_count)); \
|
||||
echo "Total drivers: $$total (root: $$root_count, waveshare-epd: $$epd_count)"
|
||||
|
||||
drivers-list:
|
||||
@{ \
|
||||
find . -mindepth 1 -maxdepth 1 -type d | grep -vE '^\./($(subst $(space),|,$(EXCLUDE_DIRS)))$$'; \
|
||||
if [ -d ./waveshare-epd ]; then find ./waveshare-epd -mindepth 1 -maxdepth 1 -type d; fi; \
|
||||
} | sed 's|^\./||' | sort
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
[](https://pkg.go.dev/tinygo.org/x/drivers) [](https://github.com/tinygo-org/drivers/actions/workflows/build.yml)
|
||||
|
||||
|
||||
This package provides a collection of over 100 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org).
|
||||
This package provides a collection of over 140 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org).
|
||||
|
||||
For the complete list, please see:
|
||||
https://tinygo.org/docs/reference/devices/
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You can help TinyGo with a financial contribution using OpenCollective. Please see https://opencollective.com/tinygo for more information. Thank you!
|
||||
|
||||
## Installing
|
||||
|
||||
```shell
|
||||
|
||||
+7
-3
@@ -5,9 +5,10 @@ package apa102 // import "tinygo.org/x/drivers/apa102"
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,8 +38,11 @@ func New(b drivers.SPI) *Device {
|
||||
|
||||
// NewSoftwareSPI returns a new APA102 driver that will use a software based
|
||||
// implementation of the SPI protocol.
|
||||
func NewSoftwareSPI(sckPin, sdoPin machine.Pin, delay uint32) *Device {
|
||||
return New(&bbSPI{SCK: sckPin, SDO: sdoPin, Delay: delay})
|
||||
func NewSoftwareSPI(sckPin, sdoPin pin.Output, delay uint32) *Device {
|
||||
return New(&bbSPI{SCK: sckPin.Set, SDO: sdoPin.Set, Delay: delay, configurePins: func() {
|
||||
legacy.ConfigurePinOut(sckPin)
|
||||
legacy.ConfigurePinOut(sdoPin)
|
||||
}})
|
||||
}
|
||||
|
||||
// WriteColors writes the given RGBA color slice out using the APA102 protocol.
|
||||
|
||||
+12
-6
@@ -1,6 +1,9 @@
|
||||
package apa102
|
||||
|
||||
import "machine"
|
||||
import (
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// bbSPI is a dumb bit-bang implementation of SPI protocol that is hardcoded
|
||||
// to mode 0 and ignores trying to receive data. Just enough for the APA102.
|
||||
@@ -8,15 +11,18 @@ import "machine"
|
||||
// most purposes other than the APA102 package. It might be desirable to make
|
||||
// this more generic and include it in the TinyGo "machine" package instead.
|
||||
type bbSPI struct {
|
||||
SCK machine.Pin
|
||||
SDO machine.Pin
|
||||
Delay uint32
|
||||
SCK pin.OutputFunc
|
||||
SDO pin.OutputFunc
|
||||
Delay uint32
|
||||
configurePins func()
|
||||
}
|
||||
|
||||
// Configure sets up the SCK and SDO pins as outputs and sets them low
|
||||
func (s *bbSPI) Configure() {
|
||||
s.SCK.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
s.SDO.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
if s.configurePins == nil {
|
||||
panic(legacy.ErrConfigBeforeInstantiated)
|
||||
}
|
||||
s.configurePins()
|
||||
s.SCK.Low()
|
||||
s.SDO.Low()
|
||||
if s.Delay == 0 {
|
||||
|
||||
+31
-24
@@ -1,31 +1,36 @@
|
||||
package bmi160
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// DeviceSPI is the SPI interface to a BMI160 accelerometer/gyroscope. There is
|
||||
// also an I2C interface, but it is not yet supported.
|
||||
type DeviceSPI struct {
|
||||
// Chip select pin
|
||||
CSB machine.Pin
|
||||
csb pin.OutputFunc
|
||||
|
||||
buf [7]byte
|
||||
|
||||
// SPI bus (requires chip select to be usable).
|
||||
Bus drivers.SPI
|
||||
bus drivers.SPI
|
||||
configurePins func()
|
||||
}
|
||||
|
||||
// NewSPI returns a new device driver. The pin and SPI interface are not
|
||||
// touched, provide a fully configured SPI object and call Configure to start
|
||||
// using this device.
|
||||
func NewSPI(csb machine.Pin, spi drivers.SPI) *DeviceSPI {
|
||||
func NewSPI(csb pin.Output, spi drivers.SPI) *DeviceSPI {
|
||||
return &DeviceSPI{
|
||||
CSB: csb, // chip select
|
||||
Bus: spi,
|
||||
csb: csb.Set, // chip select
|
||||
bus: spi,
|
||||
configurePins: func() {
|
||||
legacy.ConfigurePinOut(csb)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +38,11 @@ func NewSPI(csb machine.Pin, spi drivers.SPI) *DeviceSPI {
|
||||
// configures the BMI160, but it does not configure the SPI interface (it is
|
||||
// assumed to be up and running).
|
||||
func (d *DeviceSPI) Configure() error {
|
||||
d.CSB.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
d.CSB.High()
|
||||
|
||||
if d.configurePins == nil {
|
||||
return legacy.ErrConfigBeforeInstantiated
|
||||
}
|
||||
d.configurePins()
|
||||
d.csb.High()
|
||||
// The datasheet recommends doing a register read from address 0x7F to get
|
||||
// SPI communication going:
|
||||
// > If CSB sees a rising edge after power-up, the BMI160 interface switches
|
||||
@@ -86,9 +93,9 @@ func (d *DeviceSPI) ReadTemperature() (temperature int32, err error) {
|
||||
data[0] = 0x80 | reg_TEMPERATURE_0
|
||||
data[1] = 0
|
||||
data[2] = 0
|
||||
d.CSB.Low()
|
||||
err = d.Bus.Tx(data, data)
|
||||
d.CSB.High()
|
||||
d.csb.Low()
|
||||
err = d.bus.Tx(data, data)
|
||||
d.csb.High()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -123,9 +130,9 @@ func (d *DeviceSPI) ReadAcceleration() (x int32, y int32, z int32, err error) {
|
||||
for i := 1; i < len(data); i++ {
|
||||
data[i] = 0
|
||||
}
|
||||
d.CSB.Low()
|
||||
err = d.Bus.Tx(data, data)
|
||||
d.CSB.High()
|
||||
d.csb.Low()
|
||||
err = d.bus.Tx(data, data)
|
||||
d.csb.High()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -153,9 +160,9 @@ func (d *DeviceSPI) ReadRotation() (x int32, y int32, z int32, err error) {
|
||||
for i := 1; i < len(data); i++ {
|
||||
data[i] = 0
|
||||
}
|
||||
d.CSB.Low()
|
||||
err = d.Bus.Tx(data, data)
|
||||
d.CSB.High()
|
||||
d.csb.Low()
|
||||
err = d.bus.Tx(data, data)
|
||||
d.csb.High()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -201,9 +208,9 @@ func (d *DeviceSPI) readRegister(address uint8) uint8 {
|
||||
data := d.buf[:2]
|
||||
data[0] = 0x80 | address
|
||||
data[1] = 0
|
||||
d.CSB.Low()
|
||||
d.Bus.Tx(data, data)
|
||||
d.CSB.High()
|
||||
d.csb.Low()
|
||||
d.bus.Tx(data, data)
|
||||
d.csb.High()
|
||||
return data[1]
|
||||
}
|
||||
|
||||
@@ -217,7 +224,7 @@ func (d *DeviceSPI) writeRegister(address, data uint8) {
|
||||
buf[0] = address
|
||||
buf[1] = data
|
||||
|
||||
d.CSB.Low()
|
||||
d.Bus.Tx(buf, buf)
|
||||
d.CSB.High()
|
||||
d.csb.Low()
|
||||
d.bus.Tx(buf, buf)
|
||||
d.csb.High()
|
||||
}
|
||||
|
||||
+7
-8
@@ -23,6 +23,7 @@ type Filter uint
|
||||
type Device struct {
|
||||
bus drivers.I2C
|
||||
Address uint16
|
||||
buf [6]byte
|
||||
cali calibrationCoefficients
|
||||
Temperature Oversampling
|
||||
Pressure Oversampling
|
||||
@@ -134,8 +135,8 @@ func (d *Device) PrintCali() {
|
||||
|
||||
// ReadTemperature returns the temperature in celsius milli degrees (°C/1000).
|
||||
func (d *Device) ReadTemperature() (temperature int32, err error) {
|
||||
data, err := d.readData(REG_TEMP, 3)
|
||||
if err != nil {
|
||||
data := d.buf[:3]
|
||||
if err = d.readData(REG_TEMP, data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -158,8 +159,8 @@ func (d *Device) ReadTemperature() (temperature int32, err error) {
|
||||
// ReadPressure returns the pressure in milli pascals (mPa).
|
||||
func (d *Device) ReadPressure() (pressure int32, err error) {
|
||||
// First 3 bytes are Pressure, last 3 bytes are Temperature
|
||||
data, err := d.readData(REG_PRES, 6)
|
||||
if err != nil {
|
||||
data := d.buf[:6]
|
||||
if err = d.readData(REG_PRES, data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -203,7 +204,7 @@ func (d *Device) ReadPressure() (pressure int32, err error) {
|
||||
}
|
||||
|
||||
// readData reads n number of bytes of the specified register
|
||||
func (d *Device) readData(register int, n int) ([]byte, error) {
|
||||
func (d *Device) readData(register int, data []byte) error {
|
||||
// If not in normal mode, set the mode to FORCED mode, to prevent incorrect measurements
|
||||
// After the measurement in FORCED mode, the sensor will return to SLEEP mode
|
||||
if d.Mode != MODE_NORMAL {
|
||||
@@ -218,9 +219,7 @@ func (d *Device) readData(register int, n int) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Read the requested register
|
||||
data := make([]byte, n)
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), uint8(register), data[:])
|
||||
return data, err
|
||||
return legacy.ReadRegister(d.bus, uint8(d.Address), uint8(register), data[:])
|
||||
}
|
||||
|
||||
// convert3Bytes converts three bytes to int32
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// Package bno08x provides a TinyGo driver for the Adafruit BNO08x 9-DOF IMU sensors.
|
||||
//
|
||||
// This driver implements the CEVA SH-2 protocol over the SHTP transport layer,
|
||||
// providing access to orientation, motion, and environmental sensors.
|
||||
//
|
||||
// Datasheet: https://www.ceva-ip.com/wp-content/uploads/BNO080_085-Datasheet.pdf
|
||||
package bno08x
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// Buser is the interface that wraps I2C or SPI bus operations.
|
||||
type Buser interface {
|
||||
configure(address uint16, readChunk int) error
|
||||
read(target []byte) (int, uint32, error)
|
||||
write(data []byte) error
|
||||
softReset() error
|
||||
}
|
||||
|
||||
// Device represents a BNO08x sensor device.
|
||||
type Device struct {
|
||||
bus Buser
|
||||
resetPin pin.OutputFunc
|
||||
|
||||
hal *hal
|
||||
shtp *shtp
|
||||
sh2 *sh2Protocol
|
||||
|
||||
queue [8]SensorValue
|
||||
queueHead int
|
||||
queueTail int
|
||||
queueCount int
|
||||
|
||||
productIDs ProductIDs
|
||||
lastReset bool
|
||||
}
|
||||
|
||||
// Config holds configuration options for the device.
|
||||
type Config struct {
|
||||
// Address is the I2C address (used only for I2C bus).
|
||||
Address uint16
|
||||
|
||||
// ResetPin is the optional hardware reset pin.
|
||||
ResetPin pin.OutputFunc
|
||||
|
||||
// ReadChunk is the I2C read chunk size (used only for I2C bus).
|
||||
ReadChunk int
|
||||
|
||||
// StartupDelay is the delay after reset (default: 100ms).
|
||||
StartupDelay time.Duration
|
||||
}
|
||||
|
||||
// Configure initializes the sensor and prepares it for use.
|
||||
func (d *Device) Configure(cfg Config) error {
|
||||
// Configure bus-specific settings
|
||||
if err := d.bus.configure(cfg.Address, cfg.ReadChunk); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.ResetPin != nil {
|
||||
d.resetPin = cfg.ResetPin
|
||||
}
|
||||
if cfg.StartupDelay <= 0 {
|
||||
cfg.StartupDelay = 100 * time.Millisecond
|
||||
}
|
||||
|
||||
d.hal = newHAL(d)
|
||||
d.shtp = newSHTP(d.hal)
|
||||
d.sh2 = newSH2Protocol(d)
|
||||
|
||||
d.queueHead = 0
|
||||
d.queueTail = 0
|
||||
d.queueCount = 0
|
||||
d.productIDs = ProductIDs{}
|
||||
d.lastReset = false
|
||||
|
||||
if err := d.hal.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now that handlers are registered, perform reset
|
||||
// Try hardware reset first if available
|
||||
if d.resetPin != nil {
|
||||
d.hardwareReset()
|
||||
time.Sleep(cfg.StartupDelay)
|
||||
} else {
|
||||
// No hardware reset pin - try soft reset via bus
|
||||
if err := d.bus.softReset(); err != nil {
|
||||
// If that fails, try soft reset via SHTP protocol
|
||||
_ = d.sh2.softReset()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for reset notification by actively polling
|
||||
// The sensor should send reset complete message shortly after reset
|
||||
deadline := time.Now().Add(1000 * time.Millisecond)
|
||||
pollCount := 0
|
||||
for time.Now().Before(deadline) {
|
||||
pollCount++
|
||||
if err := d.service(); err != nil {
|
||||
// Ignore errors during initial polling - sensor might not be ready
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if d.lastReset {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
if !d.lastReset {
|
||||
return errTimeout
|
||||
}
|
||||
|
||||
// NOTE: We intentionally skip the Initialize command (sh2_initialize)
|
||||
// Testing revealed that sending the Initialize command (0xF2 0x00 0x04 0x01...)
|
||||
// prevents the BNO08x from sending sensor reports on channel 3.
|
||||
// The sensor works correctly without this command after a soft reset.
|
||||
// The Arduino library likely works because it does a hardware reset which
|
||||
// may put the sensor in a different state, or their initialization sequence
|
||||
// differs in a way that doesn't trigger this issue.
|
||||
|
||||
// Request product IDs
|
||||
if err := d.sh2.requestProductIDs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for product IDs with polling delay
|
||||
deadline = time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
if err := d.service(); err != nil {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if d.productIDs.NumEntries > 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
if d.productIDs.NumEntries == 0 {
|
||||
return errTimeout
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnableReport enables a specific sensor report at the given interval.
|
||||
func (d *Device) EnableReport(id SensorID, intervalUs uint32) error {
|
||||
err := d.sh2.enableReport(id, intervalUs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Poll a few times to let the sensor process the command
|
||||
// and potentially send acknowledgment
|
||||
for i := 0; i < 10; i++ {
|
||||
_ = d.service()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSensorConfig retrieves the current configuration for a sensor.
|
||||
func (d *Device) GetSensorConfig(id SensorID) (SensorConfig, error) {
|
||||
return d.sh2.getSensorConfig(id)
|
||||
}
|
||||
|
||||
// SetSensorConfig sets the configuration for a sensor.
|
||||
func (d *Device) SetSensorConfig(id SensorID, config SensorConfig) error {
|
||||
return d.sh2.setSensorConfig(id, config)
|
||||
}
|
||||
|
||||
// WasReset returns true if the sensor signaled a reset since the last call.
|
||||
func (d *Device) WasReset() bool {
|
||||
if d.lastReset {
|
||||
d.lastReset = false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetSensorEvent retrieves the next available sensor event if present.
|
||||
func (d *Device) GetSensorEvent() (SensorValue, bool) {
|
||||
if d.queueCount == 0 {
|
||||
if err := d.service(); err != nil {
|
||||
return SensorValue{}, false
|
||||
}
|
||||
if d.queueCount == 0 {
|
||||
return SensorValue{}, false
|
||||
}
|
||||
}
|
||||
|
||||
value := d.queue[d.queueHead]
|
||||
d.queueHead = (d.queueHead + 1) % len(d.queue)
|
||||
d.queueCount--
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
// ProductIDs returns the cached product identification information.
|
||||
func (d *Device) ProductIDs() ProductIDs {
|
||||
return d.productIDs
|
||||
}
|
||||
|
||||
// Service processes pending sensor data.
|
||||
// This is called automatically by GetSensorEvent but can be called manually
|
||||
// for more control over timing.
|
||||
func (d *Device) Service() error {
|
||||
return d.service()
|
||||
}
|
||||
|
||||
func (d *Device) enqueue(value SensorValue) {
|
||||
next := (d.queueTail + 1) % len(d.queue)
|
||||
if d.queueCount == len(d.queue) {
|
||||
// Queue full, drop oldest
|
||||
d.queueHead = (d.queueHead + 1) % len(d.queue)
|
||||
d.queueCount--
|
||||
}
|
||||
d.queue[d.queueTail] = value
|
||||
d.queueTail = next
|
||||
d.queueCount++
|
||||
}
|
||||
|
||||
func (d *Device) service() error {
|
||||
if d.shtp == nil {
|
||||
return nil
|
||||
}
|
||||
for {
|
||||
processed, err := d.shtp.poll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !processed {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) hardwareReset() {
|
||||
if d.resetPin == nil {
|
||||
return
|
||||
}
|
||||
d.resetPin.High()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.resetPin.Low()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.resetPin.High()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package bno08x
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// I2CConfig holds I2C-specific configuration options.
|
||||
type I2CConfig struct {
|
||||
// Address is the I2C address (default: 0x4A).
|
||||
Address uint16
|
||||
|
||||
// ResetPin is the optional hardware reset pin.
|
||||
ResetPin pin.OutputFunc
|
||||
|
||||
// ReadChunk is the I2C read chunk size (default: 32 bytes).
|
||||
ReadChunk int
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultAddress is the default I2C address.
|
||||
DefaultAddress = 0x4A
|
||||
)
|
||||
|
||||
// NewI2C creates a new BNO08x device using I2C communication.
|
||||
func NewI2C(bus drivers.I2C) *Device {
|
||||
return &Device{
|
||||
bus: &I2CBus{
|
||||
wire: bus,
|
||||
address: DefaultAddress,
|
||||
readChunk: i2cDefaultChunk,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// I2CBus implements the Buser interface for I2C communication.
|
||||
type I2CBus struct {
|
||||
wire drivers.I2C
|
||||
address uint16
|
||||
readChunk int
|
||||
scratch []byte
|
||||
header [shtpHeaderLength]byte
|
||||
}
|
||||
|
||||
// configure sets up the I2C bus with the specified address and chunk size.
|
||||
func (b *I2CBus) configure(address uint16, readChunk int) error {
|
||||
if address != 0 {
|
||||
b.address = address
|
||||
}
|
||||
if readChunk > 0 {
|
||||
b.readChunk = readChunk
|
||||
}
|
||||
|
||||
chunk := b.readChunk
|
||||
if chunk < shtpHeaderLength {
|
||||
chunk = shtpHeaderLength
|
||||
}
|
||||
b.scratch = make([]byte, chunk)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// read reads data from the I2C bus.
|
||||
func (b *I2CBus) read(target []byte) (int, uint32, error) {
|
||||
// Read SHTP header (4 bytes) to get packet length
|
||||
// Use pre-allocated header buffer to avoid allocations
|
||||
err := b.wire.Tx(b.address, nil, b.header[:])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// Parse packet length from header
|
||||
packetLen := uint16(b.header[0]) | (uint16(b.header[1]) << 8)
|
||||
|
||||
// Check if continuation bit is set (0x8000)
|
||||
// This means no data is available yet
|
||||
if packetLen&continueMask != 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
// No continuation bit, check for actual data
|
||||
if packetLen == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
if int(packetLen) > len(target) {
|
||||
return 0, 0, errBufferTooSmall
|
||||
}
|
||||
|
||||
// Now read the full packet in chunks, re-reading the header in first chunk
|
||||
// This follows Arduino's approach: initial header read is just to get size,
|
||||
// actual packet data (including header) is read in the loop
|
||||
cargoRemaining := int(packetLen)
|
||||
offset := 0
|
||||
firstRead := true
|
||||
|
||||
for cargoRemaining > 0 {
|
||||
var request int
|
||||
if firstRead {
|
||||
// First read: get the full packet including header (up to chunkSize)
|
||||
request = b.readChunk
|
||||
if request > cargoRemaining {
|
||||
request = cargoRemaining
|
||||
}
|
||||
} else {
|
||||
// Subsequent reads: each chunk has a 4-byte header we need to skip
|
||||
request = b.readChunk
|
||||
if request > cargoRemaining+shtpHeaderLength {
|
||||
request = cargoRemaining + shtpHeaderLength
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure scratch buffer is large enough
|
||||
if request > len(b.scratch) {
|
||||
b.scratch = make([]byte, request)
|
||||
}
|
||||
buf := b.scratch[:request]
|
||||
|
||||
// Read chunk
|
||||
err = b.wire.Tx(b.address, nil, buf)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
var cargoRead int
|
||||
if firstRead {
|
||||
// First read: copy everything including header
|
||||
cargoRead = request
|
||||
copy(target[offset:], buf[:cargoRead])
|
||||
firstRead = false
|
||||
} else {
|
||||
// Subsequent reads: skip the 4-byte header
|
||||
cargoRead = request - shtpHeaderLength
|
||||
copy(target[offset:], buf[shtpHeaderLength:shtpHeaderLength+cargoRead])
|
||||
}
|
||||
|
||||
offset += cargoRead
|
||||
cargoRemaining -= cargoRead
|
||||
}
|
||||
|
||||
// Extract timestamp from the header in the target buffer
|
||||
timestamp := uint32(target[2]) | (uint32(target[3]) << 8)
|
||||
|
||||
return int(packetLen), timestamp, nil
|
||||
}
|
||||
|
||||
// write sends data over the I2C bus.
|
||||
func (b *I2CBus) write(data []byte) error {
|
||||
return b.wire.Tx(b.address, data, nil)
|
||||
}
|
||||
|
||||
// softReset sends a soft reset command via I2C.
|
||||
func (b *I2CBus) softReset() error {
|
||||
// Send soft reset packet via I2C as per Adafruit implementation
|
||||
// Format: [length_low, length_high, channel, sequence, command]
|
||||
// This is: 5 bytes total, channel 1 (executable), command 1 (reset)
|
||||
softResetPacket := []byte{5, 0, 1, 0, 1}
|
||||
|
||||
// Try up to 5 times
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
err = b.wire.Tx(b.address, softResetPacket, nil)
|
||||
if err == nil {
|
||||
// Success - wait for sensor to process reset
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package bno08x
|
||||
|
||||
// I2C and protocol constants
|
||||
const (
|
||||
shtpHeaderLength = 4
|
||||
maxTransferOut = 256
|
||||
maxTransferIn = 384
|
||||
|
||||
i2cDefaultChunk = 32
|
||||
continueMask = 0x8000
|
||||
)
|
||||
|
||||
// SHTP channel numbers
|
||||
const (
|
||||
channelCommand = 0
|
||||
channelExecutable = 1
|
||||
channelControl = 2
|
||||
channelSensorReport = 3
|
||||
channelWakeReport = 4
|
||||
channelGyroRV = 5
|
||||
)
|
||||
|
||||
// SH-2 report IDs
|
||||
const (
|
||||
reportProdIDReq = 0xF9
|
||||
reportProdIDResp = 0xF8
|
||||
reportSetFeature = 0xFD
|
||||
reportGetFeature = 0xFE
|
||||
reportGetFeatureResp = 0xFC
|
||||
reportCommandReq = 0xF2
|
||||
reportCommandResp = 0xF1
|
||||
reportFRSWriteReq = 0xF7
|
||||
reportFRSWriteData = 0xF6
|
||||
reportFRSReadReq = 0xF4
|
||||
reportFRSReadResp = 0xF3
|
||||
reportBaseTimestamp = 0xFB
|
||||
reportTimestampReuse = 0xFA
|
||||
reportForceFlush = 0xF0
|
||||
reportFlushCompleted = 0xEF
|
||||
reportResetReq = 0xF1
|
||||
reportResetResp = 0xF0
|
||||
)
|
||||
|
||||
// SH-2 commands
|
||||
const (
|
||||
cmdErrors = 0x01
|
||||
cmdCounts = 0x02
|
||||
cmdTare = 0x03
|
||||
cmdInitialize = 0x04
|
||||
cmdFRS = 0x05
|
||||
cmdDCD = 0x06
|
||||
cmdMECal = 0x07
|
||||
cmdProdIDReq = 0x07
|
||||
cmdDCDSave = 0x09
|
||||
cmdGetOscType = 0x0A
|
||||
cmdClearDCDReset = 0x0B
|
||||
cmdCal = 0x0C
|
||||
cmdBootloader = 0x0D
|
||||
cmdInteractiveZRO = 0x0E
|
||||
|
||||
// Command parameters
|
||||
initSystem = 0x01
|
||||
initUnsolicited = 0x80
|
||||
|
||||
countsClearCounts = 0x01
|
||||
countsGetCounts = 0x00
|
||||
|
||||
tareTareNow = 0x00
|
||||
tarePersist = 0x01
|
||||
tareSetReorientation = 0x02
|
||||
|
||||
calStart = 0x00
|
||||
calFinish = 0x01
|
||||
|
||||
commandParamCount = 9
|
||||
responseValueCount = 11
|
||||
)
|
||||
|
||||
// Feature report flags
|
||||
const (
|
||||
featChangeSensitivityRelative = 0x01
|
||||
featChangeSensitivityEnabled = 0x02
|
||||
featWakeEnabled = 0x04
|
||||
featAlwaysOnEnabled = 0x08
|
||||
)
|
||||
|
||||
// Scaling factors for sensor data
|
||||
// These are derived from the Q-point encoding in the SH-2 specification
|
||||
const (
|
||||
scaleQuat = 1.0 / 16384.0 // Q14
|
||||
scaleAccel = 1.0 / 256.0 // Q8
|
||||
scaleGyro = 1.0 / 512.0 // Q9
|
||||
scaleMag = 1.0 / 16.0 // Q4
|
||||
scaleAccuracy = 1.0 / 4096.0 // Q12
|
||||
scalePressure = 1.0 / 1048576.0 // Q20
|
||||
scaleLight = 1.0 / 256.0 // Q8
|
||||
scaleHumidity = 1.0 / 256.0 // Q8
|
||||
scaleProximity = 1.0 / 16.0 // Q4
|
||||
scaleTemperature = 1.0 / 128.0 // Q7
|
||||
scaleAngle = 1.0 / 16.0 // Q4
|
||||
scaleHeartRate = 1.0 / 16.0 // Q4
|
||||
)
|
||||
|
||||
// Activity classifier codes (extended beyond standard SH-2)
|
||||
const (
|
||||
ActivityUnknown = 0
|
||||
ActivityInVehicle = 1
|
||||
ActivityOnBicycle = 2
|
||||
ActivityOnFoot = 3
|
||||
ActivityStill = 4
|
||||
ActivityTilting = 5
|
||||
ActivityWalking = 6
|
||||
ActivityRunning = 7
|
||||
ActivityOnStairs = 8
|
||||
ActivityOptionCount = 9
|
||||
)
|
||||
|
||||
// Stability classifier values
|
||||
const (
|
||||
StabilityUnknown = 0
|
||||
StabilityOnTable = 1
|
||||
StabilityStationary = 2
|
||||
StabilityStable = 3
|
||||
StabilityMotion = 4
|
||||
)
|
||||
|
||||
// Tap detector flags
|
||||
const (
|
||||
TapX = 0x01 // 1 - X axis tapped
|
||||
TapXPos = 0x02 // 2 - X positive direction
|
||||
TapY = 0x04 // 4 - Y axis tapped
|
||||
TapYPos = 0x08 // 8 - Y positive direction
|
||||
TapZ = 0x10 // 16 - Z axis tapped
|
||||
TapZPos = 0x20 // 32 - Z positive direction
|
||||
TapDouble = 0x40 // 64 - Double tap occurred
|
||||
)
|
||||
|
||||
// GUID values for SHTP
|
||||
const (
|
||||
guidSHTP = 0
|
||||
guidExecutable = 1
|
||||
guidSensorHub = 2
|
||||
)
|
||||
|
||||
// Advertisement tags
|
||||
const (
|
||||
tagNull = 0
|
||||
tagGUID = 1
|
||||
tagMaxCargoHeaderWrite = 2
|
||||
tagMaxCargoHeaderRead = 3
|
||||
tagMaxTransferWrite = 4
|
||||
tagMaxTransferRead = 5
|
||||
tagNormalChannel = 6
|
||||
tagWakeChannel = 7
|
||||
tagAppName = 8
|
||||
tagChannelName = 9
|
||||
tagAdvCount = 10
|
||||
tagAppSpecific = 0x80
|
||||
tagSH2Version = 0x80
|
||||
tagSH2ReportLengths = 0x81
|
||||
)
|
||||
|
||||
// Timeouts
|
||||
const (
|
||||
advertTimeout = 200000 // microseconds
|
||||
commandTimeout = 300000 // microseconds
|
||||
)
|
||||
|
||||
// Executable device commands
|
||||
const (
|
||||
execDeviceCmdReset = 1
|
||||
execDeviceCmdOn = 2
|
||||
execDeviceCmdSleep = 3
|
||||
)
|
||||
|
||||
// Executable device responses
|
||||
const (
|
||||
execDeviceRespResetComplete = 1
|
||||
)
|
||||
@@ -0,0 +1,316 @@
|
||||
package bno08x
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// decodeSensor decodes a sensor report payload into a SensorValue.
|
||||
func decodeSensor(payload []byte, timestamp uint32) (SensorValue, bool) {
|
||||
if len(payload) < 4 {
|
||||
return SensorValue{}, false
|
||||
}
|
||||
|
||||
value := SensorValue{
|
||||
id: SensorID(payload[0]),
|
||||
sequence: payload[1],
|
||||
status: payload[2] & 0x03,
|
||||
delay: payload[3],
|
||||
timestamp: uint64(timestamp),
|
||||
}
|
||||
|
||||
data := payload[4:]
|
||||
|
||||
switch value.id {
|
||||
case SensorRawAccelerometer:
|
||||
if len(data) >= 10 {
|
||||
value.rawAccelerometer = RawVector3{
|
||||
X: int16(binary.LittleEndian.Uint16(data[0:])),
|
||||
Y: int16(binary.LittleEndian.Uint16(data[2:])),
|
||||
Z: int16(binary.LittleEndian.Uint16(data[4:])),
|
||||
Timestamp: binary.LittleEndian.Uint32(data[6:]),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorAccelerometer:
|
||||
if len(data) >= 6 {
|
||||
value.accelerometer = Vector3{
|
||||
X: qToFloat(data[0:], scaleAccel),
|
||||
Y: qToFloat(data[2:], scaleAccel),
|
||||
Z: qToFloat(data[4:], scaleAccel),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorLinearAcceleration:
|
||||
if len(data) >= 6 {
|
||||
value.linearAcceleration = Vector3{
|
||||
X: qToFloat(data[0:], scaleAccel),
|
||||
Y: qToFloat(data[2:], scaleAccel),
|
||||
Z: qToFloat(data[4:], scaleAccel),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorGravity:
|
||||
if len(data) >= 6 {
|
||||
value.gravity = Vector3{
|
||||
X: qToFloat(data[0:], scaleAccel),
|
||||
Y: qToFloat(data[2:], scaleAccel),
|
||||
Z: qToFloat(data[4:], scaleAccel),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorRawGyroscope:
|
||||
if len(data) >= 12 {
|
||||
value.rawGyroscope = RawGyroscope{
|
||||
X: int16(binary.LittleEndian.Uint16(data[0:])),
|
||||
Y: int16(binary.LittleEndian.Uint16(data[2:])),
|
||||
Z: int16(binary.LittleEndian.Uint16(data[4:])),
|
||||
Temperature: int16(binary.LittleEndian.Uint16(data[6:])),
|
||||
Timestamp: binary.LittleEndian.Uint32(data[8:]),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorGyroscope:
|
||||
if len(data) >= 6 {
|
||||
value.gyroscope = Vector3{
|
||||
X: qToFloat(data[0:], scaleGyro),
|
||||
Y: qToFloat(data[2:], scaleGyro),
|
||||
Z: qToFloat(data[4:], scaleGyro),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorGyroscopeUncalibrated:
|
||||
if len(data) >= 12 {
|
||||
value.gyroscopeUncal = GyroscopeUncalibrated{
|
||||
X: qToFloat(data[0:], scaleGyro),
|
||||
Y: qToFloat(data[2:], scaleGyro),
|
||||
Z: qToFloat(data[4:], scaleGyro),
|
||||
BiasX: qToFloat(data[6:], scaleGyro),
|
||||
BiasY: qToFloat(data[8:], scaleGyro),
|
||||
BiasZ: qToFloat(data[10:], scaleGyro),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorRawMagnetometer:
|
||||
if len(data) >= 10 {
|
||||
value.rawMagnetometer = RawVector3{
|
||||
X: int16(binary.LittleEndian.Uint16(data[0:])),
|
||||
Y: int16(binary.LittleEndian.Uint16(data[2:])),
|
||||
Z: int16(binary.LittleEndian.Uint16(data[4:])),
|
||||
Timestamp: binary.LittleEndian.Uint32(data[6:]),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorMagneticField:
|
||||
if len(data) >= 6 {
|
||||
value.magneticField = Vector3{
|
||||
X: qToFloat(data[0:], scaleMag),
|
||||
Y: qToFloat(data[2:], scaleMag),
|
||||
Z: qToFloat(data[4:], scaleMag),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorMagneticFieldUncalibrated:
|
||||
if len(data) >= 12 {
|
||||
value.magneticFieldUncal = MagneticFieldUncalibrated{
|
||||
X: qToFloat(data[0:], scaleMag),
|
||||
Y: qToFloat(data[2:], scaleMag),
|
||||
Z: qToFloat(data[4:], scaleMag),
|
||||
BiasX: qToFloat(data[6:], scaleMag),
|
||||
BiasY: qToFloat(data[8:], scaleMag),
|
||||
BiasZ: qToFloat(data[10:], scaleMag),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorRotationVector:
|
||||
if len(data) >= 10 {
|
||||
value.quaternion = Quaternion{
|
||||
I: qToFloat(data[0:], scaleQuat),
|
||||
J: qToFloat(data[2:], scaleQuat),
|
||||
K: qToFloat(data[4:], scaleQuat),
|
||||
Real: qToFloat(data[6:], scaleQuat),
|
||||
}
|
||||
value.quaternionAccuracy = qToFloat(data[8:], scaleAccuracy)
|
||||
}
|
||||
|
||||
case SensorGameRotationVector:
|
||||
if len(data) >= 8 {
|
||||
value.quaternion = Quaternion{
|
||||
I: qToFloat(data[0:], scaleQuat),
|
||||
J: qToFloat(data[2:], scaleQuat),
|
||||
K: qToFloat(data[4:], scaleQuat),
|
||||
Real: qToFloat(data[6:], scaleQuat),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorGeomagneticRotationVector:
|
||||
if len(data) >= 10 {
|
||||
value.quaternion = Quaternion{
|
||||
I: qToFloat(data[0:], scaleQuat),
|
||||
J: qToFloat(data[2:], scaleQuat),
|
||||
K: qToFloat(data[4:], scaleQuat),
|
||||
Real: qToFloat(data[6:], scaleQuat),
|
||||
}
|
||||
value.quaternionAccuracy = qToFloat(data[8:], scaleAccuracy)
|
||||
}
|
||||
|
||||
case SensorARVRStabilizedRV:
|
||||
if len(data) >= 10 {
|
||||
value.quaternion = Quaternion{
|
||||
I: qToFloat(data[0:], scaleQuat),
|
||||
J: qToFloat(data[2:], scaleQuat),
|
||||
K: qToFloat(data[4:], scaleQuat),
|
||||
Real: qToFloat(data[6:], scaleQuat),
|
||||
}
|
||||
value.quaternionAccuracy = qToFloat(data[8:], scaleAccuracy)
|
||||
}
|
||||
|
||||
case SensorARVRStabilizedGRV:
|
||||
if len(data) >= 8 {
|
||||
value.quaternion = Quaternion{
|
||||
I: qToFloat(data[0:], scaleQuat),
|
||||
J: qToFloat(data[2:], scaleQuat),
|
||||
K: qToFloat(data[4:], scaleQuat),
|
||||
Real: qToFloat(data[6:], scaleQuat),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorGyroIntegratedRV:
|
||||
if len(data) >= 10 {
|
||||
value.quaternion = Quaternion{
|
||||
I: qToFloat(data[0:], scaleQuat),
|
||||
J: qToFloat(data[2:], scaleQuat),
|
||||
K: qToFloat(data[4:], scaleQuat),
|
||||
Real: qToFloat(data[6:], scaleQuat),
|
||||
}
|
||||
// Angular velocity X at data[8:10]
|
||||
}
|
||||
|
||||
case SensorPressure:
|
||||
if len(data) >= 4 {
|
||||
value.pressure = float32(int32(binary.LittleEndian.Uint32(data[0:]))) * scalePressure
|
||||
}
|
||||
|
||||
case SensorAmbientLight:
|
||||
if len(data) >= 4 {
|
||||
value.ambientLight = float32(int32(binary.LittleEndian.Uint32(data[0:]))) * scaleLight
|
||||
}
|
||||
|
||||
case SensorHumidity:
|
||||
if len(data) >= 2 {
|
||||
value.humidity = qToFloat(data[0:], scaleHumidity)
|
||||
}
|
||||
|
||||
case SensorProximity:
|
||||
if len(data) >= 2 {
|
||||
value.proximity = qToFloat(data[0:], scaleProximity)
|
||||
}
|
||||
|
||||
case SensorTemperature:
|
||||
if len(data) >= 2 {
|
||||
value.temperature = qToFloat(data[0:], scaleTemperature)
|
||||
}
|
||||
|
||||
case SensorTapDetector:
|
||||
if len(data) >= 1 {
|
||||
value.tapDetector = TapDetector{
|
||||
Flags: data[0],
|
||||
}
|
||||
}
|
||||
|
||||
case SensorStepDetector:
|
||||
if len(data) >= 4 {
|
||||
value.stepDetector = StepDetector{
|
||||
Latency: binary.LittleEndian.Uint32(data[0:]),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorStepCounter:
|
||||
if len(data) >= 8 {
|
||||
value.stepCounter = StepCounter{
|
||||
Count: uint16(binary.LittleEndian.Uint32(data[4:8])),
|
||||
Latency: binary.LittleEndian.Uint32(data[0:4]),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorSignificantMotion:
|
||||
if len(data) >= 2 {
|
||||
value.significantMotion = SignificantMotion{
|
||||
Motion: binary.LittleEndian.Uint16(data[0:]),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorStabilityClassifier:
|
||||
if len(data) >= 1 {
|
||||
value.stabilityClassifier = StabilityClassifier{
|
||||
Classification: data[0],
|
||||
}
|
||||
}
|
||||
|
||||
case SensorStabilityDetector:
|
||||
if len(data) >= 1 {
|
||||
value.stabilityDetector = data[0]
|
||||
}
|
||||
|
||||
case SensorShakeDetector:
|
||||
if len(data) >= 2 {
|
||||
value.shakeDetector = ShakeDetector{
|
||||
Shake: binary.LittleEndian.Uint16(data[0:]),
|
||||
}
|
||||
}
|
||||
|
||||
case SensorFlipDetector:
|
||||
if len(data) >= 2 {
|
||||
value.flipDetector = binary.LittleEndian.Uint16(data[0:2])
|
||||
}
|
||||
|
||||
case SensorPickupDetector:
|
||||
if len(data) >= 2 {
|
||||
// Pickup detected at data[0:2]
|
||||
}
|
||||
|
||||
case SensorPersonalActivityClassifier:
|
||||
if len(data) >= 16 {
|
||||
value.personalActivityClassifier = PersonalActivityClassifier{
|
||||
Page: data[0],
|
||||
MostLikelyState: data[1],
|
||||
EndOfPage: data[15],
|
||||
}
|
||||
for i := 0; i < 10 && i+2 < len(data); i++ {
|
||||
value.personalActivityClassifier.Confidence[i] = data[2+i]
|
||||
}
|
||||
}
|
||||
|
||||
case SensorSleepDetector:
|
||||
if len(data) >= 1 {
|
||||
value.sleepDetector = data[0]
|
||||
}
|
||||
|
||||
case SensorTiltDetector:
|
||||
if len(data) >= 1 {
|
||||
value.tiltDetector = data[0]
|
||||
}
|
||||
|
||||
case SensorPocketDetector:
|
||||
if len(data) >= 1 {
|
||||
value.pocketDetector = data[0]
|
||||
}
|
||||
|
||||
case SensorCircleDetector:
|
||||
if len(data) >= 1 {
|
||||
value.circleDetector = data[0]
|
||||
}
|
||||
|
||||
case SensorHeartRateMonitor:
|
||||
if len(data) >= 2 {
|
||||
value.heartRateMonitor = binary.LittleEndian.Uint16(data[0:])
|
||||
}
|
||||
}
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
// qToFloat converts a Q-point fixed-point value to float32.
|
||||
func qToFloat(data []byte, scale float32) float32 {
|
||||
if len(data) < 2 {
|
||||
return 0
|
||||
}
|
||||
return float32(int16(binary.LittleEndian.Uint16(data))) * scale
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package bno08x
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// hal implements the hardware abstraction layer for bus communication.
|
||||
type hal struct {
|
||||
device *Device
|
||||
}
|
||||
|
||||
func newHAL(dev *Device) *hal {
|
||||
return &hal{
|
||||
device: dev,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hal) open() error {
|
||||
// HAL is now open and ready for communication
|
||||
// Soft reset will be sent after handlers are registered
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hal) close() {}
|
||||
|
||||
func (h *hal) read(target []byte) (int, uint32, error) {
|
||||
return h.device.bus.read(target)
|
||||
}
|
||||
|
||||
func (h *hal) write(frame []byte) (int, error) {
|
||||
if len(frame) > maxTransferOut {
|
||||
return 0, errFrameTooLarge
|
||||
}
|
||||
err := h.device.bus.write(frame)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(frame), nil
|
||||
}
|
||||
|
||||
func (h *hal) getTimeUs() uint32 {
|
||||
return uint32(time.Now().UnixNano() / 1000)
|
||||
}
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
// SH-2 specification found at https://www.ceva-ip.com/wp-content/uploads/SH-2-Reference-Manual.pdf
|
||||
|
||||
package bno08x
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
)
|
||||
|
||||
// getReportLen returns the length in bytes of a sensor report given its ID.
|
||||
// Returns 0 for unknown report IDs.
|
||||
func getReportLen(reportID byte) int {
|
||||
switch reportID {
|
||||
case 0xF1: // FLUSH_COMPLETED
|
||||
return 6
|
||||
case 0xFA: // TIMESTAMP_REBASE
|
||||
return 5
|
||||
case 0xFB: // BASE_TIMESTAMP_REF
|
||||
return 5
|
||||
case 0xFC: // GET_FEATURE_RESP
|
||||
return 17
|
||||
case 0x01: // Accelerometer (calibrated)
|
||||
return 10
|
||||
case 0x02: // Gyroscope (calibrated)
|
||||
return 10
|
||||
case 0x03: // Magnetic field (calibrated)
|
||||
return 10
|
||||
case 0x04: // Linear acceleration
|
||||
return 10
|
||||
case 0x05: // Rotation vector
|
||||
return 14
|
||||
case 0x06: // Gravity
|
||||
return 10
|
||||
case 0x07: // Gyroscope uncalibrated
|
||||
return 16
|
||||
case 0x08: // Game rotation vector
|
||||
return 12
|
||||
case 0x09: // Geomagnetic rotation vector
|
||||
return 14
|
||||
case 0x0A: // Pressure
|
||||
return 10
|
||||
case 0x0B: // Ambient light
|
||||
return 10
|
||||
case 0x0C: // Humidity
|
||||
return 10
|
||||
case 0x0D: // Proximity
|
||||
return 10
|
||||
case 0x0E: // Temperature
|
||||
return 10
|
||||
case 0x0F: // Magnetic field uncalibrated
|
||||
return 16
|
||||
case 0x10: // Tap detector
|
||||
return 5
|
||||
case 0x11: // Step counter
|
||||
return 12
|
||||
case 0x12: // Significant motion
|
||||
return 6
|
||||
case 0x13: // Stability classifier
|
||||
return 5
|
||||
case 0x14: // Raw accelerometer
|
||||
return 16
|
||||
case 0x15: // Raw gyroscope
|
||||
return 16
|
||||
case 0x16: // Raw magnetometer
|
||||
return 16
|
||||
case 0x18: // Step detector
|
||||
return 8
|
||||
case 0x19: // Shake detector
|
||||
return 6
|
||||
case 0x1A: // Flip detector
|
||||
return 6
|
||||
case 0x1B: // Pickup detector
|
||||
return 6
|
||||
case 0x1C: // Stability detector
|
||||
return 6
|
||||
case 0x1E: // Personal activity classifier
|
||||
return 16
|
||||
default:
|
||||
// For most sensor reports, they are typically 10-16 bytes
|
||||
// If we don't know the exact length, return a safe default
|
||||
// that covers most cases (the handler will bounds-check)
|
||||
if reportID < 0xF0 {
|
||||
return 10 // Most sensor reports are at least this long
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// sh2Protocol implements the Sensor Hub 2 (SH-2) application protocol.
|
||||
type sh2Protocol struct {
|
||||
device *Device
|
||||
transport *shtp
|
||||
cmdSeq uint8
|
||||
waiting bool
|
||||
lastCmd uint8
|
||||
pendingConfigRequest bool
|
||||
pendingConfigSensor SensorID
|
||||
receivedConfig SensorConfig
|
||||
configReady bool
|
||||
configBuf [17]byte // Reusable buffer for setSensorConfig
|
||||
commandBuf [3 + commandParamCount]byte // Reusable buffer for sendCommand
|
||||
}
|
||||
|
||||
func newSH2Protocol(device *Device) *sh2Protocol {
|
||||
proto := &sh2Protocol{
|
||||
device: device,
|
||||
transport: device.shtp,
|
||||
}
|
||||
|
||||
// Register handlers for each channel
|
||||
device.shtp.register(channelControl, proto.handleControl)
|
||||
device.shtp.register(channelSensorReport, proto.handleSensor)
|
||||
device.shtp.register(channelWakeReport, proto.handleSensor)
|
||||
device.shtp.register(channelGyroRV, proto.handleSensor)
|
||||
device.shtp.register(channelExecutable, proto.handleExecutable)
|
||||
|
||||
return proto
|
||||
}
|
||||
|
||||
// softReset sends a software reset command to the sensor.
|
||||
func (s *sh2Protocol) softReset() error {
|
||||
payload := []byte{execDeviceCmdReset}
|
||||
return s.transport.send(channelExecutable, payload)
|
||||
}
|
||||
|
||||
// initialize sends the initialize command to the sensor.
|
||||
func (s *sh2Protocol) initialize() error {
|
||||
return s.sendCommand(cmdInitialize, []byte{initSystem})
|
||||
}
|
||||
|
||||
// requestProductIDs requests product identification information.
|
||||
func (s *sh2Protocol) requestProductIDs() error {
|
||||
payload := []byte{reportProdIDReq, 0x00}
|
||||
return s.transport.send(channelControl, payload)
|
||||
}
|
||||
|
||||
// enableReport enables a sensor report at the specified interval.
|
||||
func (s *sh2Protocol) enableReport(id SensorID, intervalUs uint32) error {
|
||||
config := SensorConfig{
|
||||
ReportInterval: intervalUs,
|
||||
}
|
||||
return s.setSensorConfig(id, config)
|
||||
}
|
||||
|
||||
// getSensorConfig retrieves the configuration for a sensor.
|
||||
// This method sends a GET_FEATURE request and waits for the response
|
||||
// by polling the device. It will timeout after approximately 1 second.
|
||||
func (s *sh2Protocol) getSensorConfig(id SensorID) (SensorConfig, error) {
|
||||
// Mark that we're waiting for a config response
|
||||
s.pendingConfigRequest = true
|
||||
s.pendingConfigSensor = id
|
||||
s.configReady = false
|
||||
|
||||
payload := []byte{reportGetFeature, byte(id)}
|
||||
err := s.transport.send(channelControl, payload)
|
||||
if err != nil {
|
||||
s.pendingConfigRequest = false
|
||||
return SensorConfig{}, err
|
||||
}
|
||||
|
||||
// Poll for response with timeout
|
||||
maxAttempts := 100 // ~1 second with 10ms delays
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
// Service the device to process incoming messages
|
||||
s.device.shtp.poll()
|
||||
|
||||
if s.configReady {
|
||||
s.pendingConfigRequest = false
|
||||
s.configReady = false
|
||||
return s.receivedConfig, nil
|
||||
}
|
||||
|
||||
// Small delay between polls
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
s.pendingConfigRequest = false
|
||||
return SensorConfig{}, errTimeout
|
||||
}
|
||||
|
||||
// setSensorConfig configures a sensor.
|
||||
func (s *sh2Protocol) setSensorConfig(id SensorID, config SensorConfig) error {
|
||||
// Use pre-allocated buffer to avoid allocations
|
||||
payload := s.configBuf[:]
|
||||
payload[0] = reportSetFeature
|
||||
payload[1] = byte(id)
|
||||
|
||||
// Build feature flags
|
||||
var flags uint8
|
||||
if config.ChangeSensitivityEnabled {
|
||||
flags |= featChangeSensitivityEnabled
|
||||
}
|
||||
if config.ChangeSensitivityRelative {
|
||||
flags |= featChangeSensitivityRelative
|
||||
}
|
||||
if config.WakeupEnabled {
|
||||
flags |= featWakeEnabled
|
||||
}
|
||||
if config.AlwaysOnEnabled {
|
||||
flags |= featAlwaysOnEnabled
|
||||
}
|
||||
payload[2] = flags
|
||||
|
||||
binary.LittleEndian.PutUint16(payload[3:5], config.ChangeSensitivity)
|
||||
binary.LittleEndian.PutUint32(payload[5:9], config.ReportInterval)
|
||||
binary.LittleEndian.PutUint32(payload[9:13], config.BatchInterval)
|
||||
binary.LittleEndian.PutUint32(payload[13:17], config.SensorSpecific)
|
||||
|
||||
return s.transport.send(channelControl, payload)
|
||||
}
|
||||
|
||||
// sendCommand sends a command with parameters to the sensor.
|
||||
func (s *sh2Protocol) sendCommand(command byte, params []byte) error {
|
||||
// Use pre-allocated buffer to avoid allocations
|
||||
payload := s.commandBuf[:]
|
||||
payload[0] = reportCommandReq
|
||||
payload[1] = s.cmdSeq
|
||||
payload[2] = command
|
||||
s.cmdSeq++
|
||||
s.lastCmd = command
|
||||
s.waiting = true
|
||||
|
||||
for i := 0; i < commandParamCount && i < len(params); i++ {
|
||||
payload[3+i] = params[i]
|
||||
}
|
||||
|
||||
return s.transport.send(channelControl, payload[:3+commandParamCount])
|
||||
}
|
||||
|
||||
// handleControl processes control channel messages.
|
||||
func (s *sh2Protocol) handleControl(payload []byte, timestamp uint32) {
|
||||
if len(payload) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
reportID := payload[0]
|
||||
|
||||
switch reportID {
|
||||
case reportProdIDResp:
|
||||
s.handleProdID(payload, timestamp)
|
||||
case reportCommandResp:
|
||||
s.handleCommandResp(payload, timestamp)
|
||||
case reportGetFeatureResp:
|
||||
s.handleGetFeatureResp(payload, timestamp)
|
||||
case reportFRSReadResp:
|
||||
// FRS (Flash Record System) read response
|
||||
// Not implemented in basic version
|
||||
}
|
||||
}
|
||||
|
||||
// handleProdID processes product ID responses.
|
||||
func (s *sh2Protocol) handleProdID(payload []byte, timestamp uint32) {
|
||||
if len(payload) < 16 {
|
||||
return
|
||||
}
|
||||
|
||||
entry := ProductID{
|
||||
ResetCause: payload[1],
|
||||
VersionMajor: payload[2],
|
||||
VersionMinor: payload[3],
|
||||
PartNumber: binary.LittleEndian.Uint32(payload[4:8]),
|
||||
BuildNumber: binary.LittleEndian.Uint32(payload[8:12]),
|
||||
VersionPatch: binary.LittleEndian.Uint16(payload[12:14]),
|
||||
Reserved0: payload[14],
|
||||
Reserved1: payload[15],
|
||||
}
|
||||
|
||||
// Store in first slot
|
||||
s.device.productIDs.Entries[0] = entry
|
||||
s.device.productIDs.NumEntries = 1
|
||||
}
|
||||
|
||||
// handleCommandResp processes command responses.
|
||||
func (s *sh2Protocol) handleCommandResp(payload []byte, timestamp uint32) {
|
||||
if len(payload) < 16 {
|
||||
return
|
||||
}
|
||||
|
||||
// seq := payload[1]
|
||||
command := payload[2]
|
||||
// commandSeq := payload[3]
|
||||
// respSeq := payload[4]
|
||||
|
||||
// Check if this response is for our command
|
||||
if s.waiting && command == s.lastCmd {
|
||||
s.waiting = false
|
||||
// Status is in payload[6]
|
||||
// For now, we just acknowledge receipt
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetFeatureResp processes get feature responses.
|
||||
func (s *sh2Protocol) handleGetFeatureResp(payload []byte, timestamp uint32) {
|
||||
if len(payload) < 17 {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
sensorID := SensorID(payload[1])
|
||||
flags := payload[2]
|
||||
changeSensitivity := binary.LittleEndian.Uint16(payload[3:5])
|
||||
reportInterval := binary.LittleEndian.Uint32(payload[5:9])
|
||||
batchInterval := binary.LittleEndian.Uint32(payload[9:13])
|
||||
sensorSpecific := binary.LittleEndian.Uint32(payload[13:17])
|
||||
|
||||
// If we're waiting for this sensor's config, store it
|
||||
if s.pendingConfigRequest && s.pendingConfigSensor == sensorID {
|
||||
s.receivedConfig = SensorConfig{
|
||||
ChangeSensitivityEnabled: flags&featChangeSensitivityEnabled != 0,
|
||||
ChangeSensitivityRelative: flags&featChangeSensitivityRelative != 0,
|
||||
WakeupEnabled: flags&featWakeEnabled != 0,
|
||||
AlwaysOnEnabled: flags&featAlwaysOnEnabled != 0,
|
||||
ChangeSensitivity: changeSensitivity,
|
||||
ReportInterval: reportInterval,
|
||||
BatchInterval: batchInterval,
|
||||
SensorSpecific: sensorSpecific,
|
||||
}
|
||||
s.configReady = true
|
||||
}
|
||||
}
|
||||
|
||||
// handleSensor processes sensor report messages.
|
||||
// The payload can contain multiple sensor reports batched together.
|
||||
func (s *sh2Protocol) handleSensor(payload []byte, timestamp uint32) {
|
||||
cursor := 0
|
||||
var referenceDelta uint32
|
||||
|
||||
for cursor < len(payload) {
|
||||
if cursor >= len(payload) {
|
||||
break
|
||||
}
|
||||
|
||||
reportID := payload[cursor]
|
||||
reportLen := getReportLen(reportID)
|
||||
|
||||
if reportLen == 0 {
|
||||
// Unknown report ID
|
||||
break
|
||||
}
|
||||
|
||||
if cursor+reportLen > len(payload) {
|
||||
// Not enough data for this report
|
||||
break
|
||||
}
|
||||
|
||||
// Handle special report types
|
||||
switch reportID {
|
||||
case 0xFB: // SENSORHUB_BASE_TIMESTAMP_REF
|
||||
if reportLen >= 5 {
|
||||
// Extract timebase (little-endian uint32)
|
||||
timebase := binary.LittleEndian.Uint32(payload[cursor+1 : cursor+5])
|
||||
referenceDelta = -timebase // Store negative for delta calculation
|
||||
}
|
||||
|
||||
case 0xFA: // SENSORHUB_TIMESTAMP_REBASE
|
||||
if reportLen >= 5 {
|
||||
timebase := binary.LittleEndian.Uint32(payload[cursor+1 : cursor+5])
|
||||
referenceDelta += timebase
|
||||
}
|
||||
|
||||
case 0xF1: // SENSORHUB_FLUSH_COMPLETED
|
||||
// Route to control handler
|
||||
s.handleControl(payload[cursor:cursor+reportLen], timestamp)
|
||||
|
||||
default:
|
||||
// Regular sensor report
|
||||
value, ok := decodeSensor(payload[cursor:cursor+reportLen], timestamp)
|
||||
if ok {
|
||||
s.device.enqueue(value)
|
||||
}
|
||||
}
|
||||
|
||||
cursor += reportLen
|
||||
}
|
||||
} // handleExecutable processes executable channel messages.
|
||||
func (s *sh2Protocol) handleExecutable(payload []byte, timestamp uint32) {
|
||||
if len(payload) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
reportID := payload[0]
|
||||
|
||||
switch reportID {
|
||||
case execDeviceRespResetComplete:
|
||||
s.device.lastReset = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// SHTP specification found at https://www.ceva-ip.com/wp-content/uploads/SH-2-SHTP-Reference-Manual.pdf
|
||||
|
||||
package bno08x
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// shtpHandler is a callback for handling SHTP channel data.
|
||||
type shtpHandler func(payload []byte, timestamp uint32)
|
||||
|
||||
// shtp implements the Sensor Hub Transport Protocol layer.
|
||||
type shtp struct {
|
||||
hal *hal
|
||||
handlers map[uint8]shtpHandler
|
||||
seq [8]uint8
|
||||
rx [maxTransferIn]byte // Reusable receive buffer
|
||||
tx [maxTransferOut]byte // Reusable transmit buffer
|
||||
}
|
||||
|
||||
func newSHTP(hal *hal) *shtp {
|
||||
return &shtp{
|
||||
hal: hal,
|
||||
handlers: make(map[uint8]shtpHandler),
|
||||
}
|
||||
}
|
||||
|
||||
// register registers a handler for a specific SHTP channel.
|
||||
func (s *shtp) register(channel uint8, handler shtpHandler) {
|
||||
if handler == nil {
|
||||
delete(s.handlers, channel)
|
||||
return
|
||||
}
|
||||
s.handlers[channel] = handler
|
||||
}
|
||||
|
||||
// send transmits a payload on the specified channel.
|
||||
func (s *shtp) send(channel uint8, payload []byte) error {
|
||||
total := len(payload) + shtpHeaderLength
|
||||
if total > maxTransferOut {
|
||||
return errFrameTooLarge
|
||||
}
|
||||
|
||||
// Use pre-allocated transmit buffer to avoid allocations
|
||||
frame := s.tx[:total]
|
||||
binary.LittleEndian.PutUint16(frame[0:2], uint16(total))
|
||||
frame[2] = channel
|
||||
frame[3] = s.seq[channel]
|
||||
s.seq[channel]++
|
||||
copy(frame[shtpHeaderLength:], payload)
|
||||
|
||||
_, err := s.hal.write(frame)
|
||||
return err
|
||||
}
|
||||
|
||||
// poll checks for and processes incoming SHTP packets.
|
||||
// Returns true if a packet was processed, false if no data available.
|
||||
func (s *shtp) poll() (bool, error) {
|
||||
n, timestamp, err := s.hal.read(s.rx[:])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if n == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
packet := s.rx[:n]
|
||||
length := int(binary.LittleEndian.Uint16(packet[0:2]) & ^uint16(continueMask))
|
||||
if length > n {
|
||||
length = n
|
||||
}
|
||||
if length < shtpHeaderLength {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
channel := packet[2]
|
||||
// seq := packet[3] // sequence number, not currently validated
|
||||
payload := packet[shtpHeaderLength:length]
|
||||
|
||||
if handler := s.handlers[channel]; handler != nil {
|
||||
handler(payload, timestamp)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
+572
@@ -0,0 +1,572 @@
|
||||
package bno08x
|
||||
|
||||
// SensorID identifies a specific sensor type.
|
||||
type SensorID uint8
|
||||
|
||||
// Sensor IDs as defined in the SH-2 specification.
|
||||
const (
|
||||
SensorRawAccelerometer SensorID = 0x14
|
||||
SensorAccelerometer SensorID = 0x01
|
||||
SensorLinearAcceleration SensorID = 0x04
|
||||
SensorGravity SensorID = 0x06
|
||||
SensorRawGyroscope SensorID = 0x15
|
||||
SensorGyroscope SensorID = 0x02
|
||||
SensorGyroscopeUncalibrated SensorID = 0x07
|
||||
SensorRawMagnetometer SensorID = 0x16
|
||||
SensorMagneticField SensorID = 0x03
|
||||
SensorMagneticFieldUncalibrated SensorID = 0x0F
|
||||
SensorRotationVector SensorID = 0x05
|
||||
SensorGameRotationVector SensorID = 0x08
|
||||
SensorGeomagneticRotationVector SensorID = 0x09
|
||||
SensorPressure SensorID = 0x0A
|
||||
SensorAmbientLight SensorID = 0x0B
|
||||
SensorHumidity SensorID = 0x0C
|
||||
SensorProximity SensorID = 0x0D
|
||||
SensorTemperature SensorID = 0x0E
|
||||
SensorReserved SensorID = 0x17
|
||||
SensorTapDetector SensorID = 0x10
|
||||
SensorStepDetector SensorID = 0x18
|
||||
SensorStepCounter SensorID = 0x11
|
||||
SensorSignificantMotion SensorID = 0x12
|
||||
SensorStabilityClassifier SensorID = 0x13
|
||||
SensorShakeDetector SensorID = 0x19
|
||||
SensorFlipDetector SensorID = 0x1A
|
||||
SensorPickupDetector SensorID = 0x1B
|
||||
SensorStabilityDetector SensorID = 0x1C
|
||||
SensorPersonalActivityClassifier SensorID = 0x1E
|
||||
SensorSleepDetector SensorID = 0x1F
|
||||
SensorTiltDetector SensorID = 0x20
|
||||
SensorPocketDetector SensorID = 0x21
|
||||
SensorCircleDetector SensorID = 0x22
|
||||
SensorHeartRateMonitor SensorID = 0x23
|
||||
SensorARVRStabilizedRV SensorID = 0x28
|
||||
SensorARVRStabilizedGRV SensorID = 0x29
|
||||
SensorGyroIntegratedRV SensorID = 0x2A
|
||||
SensorIZROMotionRequest SensorID = 0x2B
|
||||
SensorMaxID SensorID = 0x2B
|
||||
)
|
||||
|
||||
// ProductID contains firmware information from the sensor.
|
||||
type ProductID struct {
|
||||
ResetCause uint8
|
||||
VersionMajor uint8
|
||||
VersionMinor uint8
|
||||
PartNumber uint32
|
||||
BuildNumber uint32
|
||||
VersionPatch uint16
|
||||
Reserved0 uint8
|
||||
Reserved1 uint8
|
||||
}
|
||||
|
||||
// ProductIDs holds all product ID entries returned by the sensor.
|
||||
type ProductIDs struct {
|
||||
Entries [5]ProductID
|
||||
NumEntries uint8
|
||||
}
|
||||
|
||||
// Vector3 represents a 3D vector.
|
||||
type Vector3 struct {
|
||||
X float32
|
||||
Y float32
|
||||
Z float32
|
||||
}
|
||||
|
||||
// Quaternion represents a quaternion in (real, i, j, k) format.
|
||||
// Note: This maps to (w, x, y, z) convention where w=real, x=i, y=j, z=k.
|
||||
type Quaternion struct {
|
||||
Real float32
|
||||
I float32
|
||||
J float32
|
||||
K float32
|
||||
}
|
||||
|
||||
// RawVector3 contains raw ADC counts with timestamp.
|
||||
type RawVector3 struct {
|
||||
X int16
|
||||
Y int16
|
||||
Z int16
|
||||
Timestamp uint32
|
||||
}
|
||||
|
||||
// RawGyroscope contains raw gyro readings with temperature and timestamp.
|
||||
type RawGyroscope struct {
|
||||
X int16
|
||||
Y int16
|
||||
Z int16
|
||||
Temperature int16
|
||||
Timestamp uint32
|
||||
}
|
||||
|
||||
// GyroscopeUncalibrated contains uncalibrated gyroscope data with bias.
|
||||
type GyroscopeUncalibrated struct {
|
||||
X float32
|
||||
Y float32
|
||||
Z float32
|
||||
BiasX float32
|
||||
BiasY float32
|
||||
BiasZ float32
|
||||
}
|
||||
|
||||
// MagneticFieldUncalibrated contains uncalibrated magnetometer data with bias.
|
||||
type MagneticFieldUncalibrated struct {
|
||||
X float32
|
||||
Y float32
|
||||
Z float32
|
||||
BiasX float32
|
||||
BiasY float32
|
||||
BiasZ float32
|
||||
}
|
||||
|
||||
// TapDetector contains tap/double-tap detection flags.
|
||||
type TapDetector struct {
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
// StepDetector contains step detection with latency.
|
||||
type StepDetector struct {
|
||||
Latency uint32
|
||||
}
|
||||
|
||||
// StepCounter contains step count with latency.
|
||||
type StepCounter struct {
|
||||
Count uint16
|
||||
Latency uint32
|
||||
}
|
||||
|
||||
// SignificantMotion indicates significant motion was detected.
|
||||
type SignificantMotion struct {
|
||||
Motion uint16
|
||||
}
|
||||
|
||||
// ActivityClassification contains activity classification data.
|
||||
type ActivityClassification struct {
|
||||
Page uint8
|
||||
MostLikelyState uint8
|
||||
Classification [10]uint8
|
||||
EndOfPage uint8
|
||||
}
|
||||
|
||||
// ShakeDetector contains shake detection data.
|
||||
type ShakeDetector struct {
|
||||
Shake uint16
|
||||
}
|
||||
|
||||
// StabilityClassifier contains stability classification.
|
||||
type StabilityClassifier struct {
|
||||
Classification uint8
|
||||
}
|
||||
|
||||
// PersonalActivityClassifier contains personal activity data.
|
||||
type PersonalActivityClassifier struct {
|
||||
Page uint8
|
||||
MostLikelyState uint8
|
||||
Confidence [10]uint8
|
||||
EndOfPage uint8
|
||||
}
|
||||
|
||||
// SensorValue contains decoded sensor data for all sensor types.
|
||||
type SensorValue struct {
|
||||
id SensorID
|
||||
status uint8
|
||||
sequence uint8
|
||||
delay uint8
|
||||
timestamp uint64
|
||||
|
||||
// Orientation data (quaternions)
|
||||
quaternion Quaternion
|
||||
quaternionAccuracy float32
|
||||
|
||||
// Linear measurements
|
||||
accelerometer Vector3
|
||||
linearAcceleration Vector3
|
||||
gravity Vector3
|
||||
gyroscope Vector3
|
||||
gyroscopeUncal GyroscopeUncalibrated
|
||||
magneticField Vector3
|
||||
magneticFieldUncal MagneticFieldUncalibrated
|
||||
|
||||
// Raw sensor data
|
||||
rawAccelerometer RawVector3
|
||||
rawGyroscope RawGyroscope
|
||||
rawMagnetometer RawVector3
|
||||
|
||||
// Environmental sensors
|
||||
pressure float32 // hPa
|
||||
ambientLight float32 // lux
|
||||
humidity float32 // %
|
||||
proximity float32 // cm
|
||||
temperature float32 // °C
|
||||
|
||||
// Activity detection
|
||||
tapDetector TapDetector
|
||||
stepCounter StepCounter
|
||||
stepDetector StepDetector
|
||||
significantMotion SignificantMotion
|
||||
shakeDetector ShakeDetector
|
||||
flipDetector uint16
|
||||
stabilityClassifier StabilityClassifier
|
||||
stabilityDetector uint8
|
||||
activityClassifier ActivityClassification
|
||||
personalActivityClassifier PersonalActivityClassifier
|
||||
sleepDetector uint8
|
||||
tiltDetector uint8
|
||||
pocketDetector uint8
|
||||
circleDetector uint8
|
||||
heartRateMonitor uint16
|
||||
}
|
||||
|
||||
// SensorConfig holds configuration settings for a sensor.
|
||||
type SensorConfig struct {
|
||||
ChangeSensitivityEnabled bool
|
||||
ChangeSensitivityRelative bool
|
||||
WakeupEnabled bool
|
||||
AlwaysOnEnabled bool
|
||||
ChangeSensitivity uint16
|
||||
ReportInterval uint32 // microseconds
|
||||
BatchInterval uint32 // microseconds
|
||||
SensorSpecific uint32
|
||||
}
|
||||
|
||||
// Error represents a driver error.
|
||||
type Error string
|
||||
|
||||
func (e Error) Error() string { return string(e) }
|
||||
|
||||
// Error constants.
|
||||
var (
|
||||
errBufferTooSmall = Error("bno08x: buffer too small")
|
||||
errNoEvent = Error("bno08x: no sensor event available")
|
||||
errTimeout = Error("bno08x: operation timed out")
|
||||
errFrameTooLarge = Error("bno08x: frame exceeds maximum size")
|
||||
errNoBus = Error("bno08x: I2C bus not configured")
|
||||
errInvalidParam = Error("bno08x: invalid parameter")
|
||||
errHubError = Error("bno08x: sensor hub error")
|
||||
errIO = Error("bno08x: I/O error")
|
||||
)
|
||||
|
||||
// Metadata accessor methods (always available for any sensor type)
|
||||
|
||||
// ID returns the sensor ID.
|
||||
func (sv SensorValue) ID() SensorID {
|
||||
return sv.id
|
||||
}
|
||||
|
||||
// Status returns the sensor status flags.
|
||||
func (sv SensorValue) Status() uint8 {
|
||||
return sv.status
|
||||
}
|
||||
|
||||
// Sequence returns the sequence number.
|
||||
func (sv SensorValue) Sequence() uint8 {
|
||||
return sv.sequence
|
||||
}
|
||||
|
||||
// Delay returns the sensor delay value.
|
||||
func (sv SensorValue) Delay() uint8 {
|
||||
return sv.delay
|
||||
}
|
||||
|
||||
// Timestamp returns the sensor timestamp.
|
||||
func (sv SensorValue) Timestamp() uint64 {
|
||||
return sv.timestamp
|
||||
}
|
||||
|
||||
// Orientation data accessor methods
|
||||
|
||||
// Quaternion returns the quaternion value for rotation vector sensors.
|
||||
// Panics if called on a sensor type that doesn't provide quaternion data.
|
||||
func (sv SensorValue) Quaternion() Quaternion {
|
||||
switch sv.id {
|
||||
case SensorRotationVector, SensorGameRotationVector, SensorGeomagneticRotationVector,
|
||||
SensorARVRStabilizedRV, SensorARVRStabilizedGRV, SensorGyroIntegratedRV:
|
||||
return sv.quaternion
|
||||
default:
|
||||
panic("bno08x: Quaternion() called on non-rotation sensor type")
|
||||
}
|
||||
}
|
||||
|
||||
// QuaternionAccuracy returns the quaternion accuracy estimate.
|
||||
// Panics if called on a sensor type that doesn't provide quaternion accuracy.
|
||||
func (sv SensorValue) QuaternionAccuracy() float32 {
|
||||
switch sv.id {
|
||||
case SensorRotationVector, SensorGeomagneticRotationVector, SensorARVRStabilizedRV:
|
||||
return sv.quaternionAccuracy
|
||||
default:
|
||||
panic("bno08x: QuaternionAccuracy() called on sensor type without accuracy data")
|
||||
}
|
||||
}
|
||||
|
||||
// Linear measurement accessor methods
|
||||
|
||||
// Accelerometer returns the accelerometer vector.
|
||||
// Panics if called on a sensor type other than SensorAccelerometer.
|
||||
func (sv SensorValue) Accelerometer() Vector3 {
|
||||
if sv.id != SensorAccelerometer {
|
||||
panic("bno08x: Accelerometer() called on non-accelerometer sensor type")
|
||||
}
|
||||
return sv.accelerometer
|
||||
}
|
||||
|
||||
// LinearAcceleration returns the linear acceleration vector.
|
||||
// Panics if called on a sensor type other than SensorLinearAcceleration.
|
||||
func (sv SensorValue) LinearAcceleration() Vector3 {
|
||||
if sv.id != SensorLinearAcceleration {
|
||||
panic("bno08x: LinearAcceleration() called on wrong sensor type")
|
||||
}
|
||||
return sv.linearAcceleration
|
||||
}
|
||||
|
||||
// Gravity returns the gravity vector.
|
||||
// Panics if called on a sensor type other than SensorGravity.
|
||||
func (sv SensorValue) Gravity() Vector3 {
|
||||
if sv.id != SensorGravity {
|
||||
panic("bno08x: Gravity() called on non-gravity sensor type")
|
||||
}
|
||||
return sv.gravity
|
||||
}
|
||||
|
||||
// Gyroscope returns the gyroscope vector.
|
||||
// Panics if called on a sensor type other than SensorGyroscope.
|
||||
func (sv SensorValue) Gyroscope() Vector3 {
|
||||
if sv.id != SensorGyroscope {
|
||||
panic("bno08x: Gyroscope() called on non-gyroscope sensor type")
|
||||
}
|
||||
return sv.gyroscope
|
||||
}
|
||||
|
||||
// GyroscopeUncal returns the uncalibrated gyroscope data.
|
||||
// Panics if called on a sensor type other than SensorGyroscopeUncalibrated.
|
||||
func (sv SensorValue) GyroscopeUncal() GyroscopeUncalibrated {
|
||||
if sv.id != SensorGyroscopeUncalibrated {
|
||||
panic("bno08x: GyroscopeUncal() called on wrong sensor type")
|
||||
}
|
||||
return sv.gyroscopeUncal
|
||||
}
|
||||
|
||||
// MagneticField returns the magnetic field vector.
|
||||
// Panics if called on a sensor type other than SensorMagneticField.
|
||||
func (sv SensorValue) MagneticField() Vector3 {
|
||||
if sv.id != SensorMagneticField {
|
||||
panic("bno08x: MagneticField() called on wrong sensor type")
|
||||
}
|
||||
return sv.magneticField
|
||||
}
|
||||
|
||||
// MagneticFieldUncal returns the uncalibrated magnetic field data.
|
||||
// Panics if called on a sensor type other than SensorMagneticFieldUncalibrated.
|
||||
func (sv SensorValue) MagneticFieldUncal() MagneticFieldUncalibrated {
|
||||
if sv.id != SensorMagneticFieldUncalibrated {
|
||||
panic("bno08x: MagneticFieldUncal() called on wrong sensor type")
|
||||
}
|
||||
return sv.magneticFieldUncal
|
||||
}
|
||||
|
||||
// Raw sensor data accessor methods
|
||||
|
||||
// RawAccelerometer returns the raw accelerometer data.
|
||||
// Panics if called on a sensor type other than SensorRawAccelerometer.
|
||||
func (sv SensorValue) RawAccelerometer() RawVector3 {
|
||||
if sv.id != SensorRawAccelerometer {
|
||||
panic("bno08x: RawAccelerometer() called on wrong sensor type")
|
||||
}
|
||||
return sv.rawAccelerometer
|
||||
}
|
||||
|
||||
// RawGyroscope returns the raw gyroscope data.
|
||||
// Panics if called on a sensor type other than SensorRawGyroscope.
|
||||
func (sv SensorValue) RawGyroscope() RawGyroscope {
|
||||
if sv.id != SensorRawGyroscope {
|
||||
panic("bno08x: RawGyroscope() called on wrong sensor type")
|
||||
}
|
||||
return sv.rawGyroscope
|
||||
}
|
||||
|
||||
// RawMagnetometer returns the raw magnetometer data.
|
||||
// Panics if called on a sensor type other than SensorRawMagnetometer.
|
||||
func (sv SensorValue) RawMagnetometer() RawVector3 {
|
||||
if sv.id != SensorRawMagnetometer {
|
||||
panic("bno08x: RawMagnetometer() called on wrong sensor type")
|
||||
}
|
||||
return sv.rawMagnetometer
|
||||
}
|
||||
|
||||
// Environmental sensor accessor methods
|
||||
|
||||
// Pressure returns the pressure reading in hPa.
|
||||
// Panics if called on a sensor type other than SensorPressure.
|
||||
func (sv SensorValue) Pressure() float32 {
|
||||
if sv.id != SensorPressure {
|
||||
panic("bno08x: Pressure() called on non-pressure sensor type")
|
||||
}
|
||||
return sv.pressure
|
||||
}
|
||||
|
||||
// AmbientLight returns the ambient light reading in lux.
|
||||
// Panics if called on a sensor type other than SensorAmbientLight.
|
||||
func (sv SensorValue) AmbientLight() float32 {
|
||||
if sv.id != SensorAmbientLight {
|
||||
panic("bno08x: AmbientLight() called on wrong sensor type")
|
||||
}
|
||||
return sv.ambientLight
|
||||
}
|
||||
|
||||
// Humidity returns the humidity reading in percent.
|
||||
// Panics if called on a sensor type other than SensorHumidity.
|
||||
func (sv SensorValue) Humidity() float32 {
|
||||
if sv.id != SensorHumidity {
|
||||
panic("bno08x: Humidity() called on non-humidity sensor type")
|
||||
}
|
||||
return sv.humidity
|
||||
}
|
||||
|
||||
// Proximity returns the proximity reading in cm.
|
||||
// Panics if called on a sensor type other than SensorProximity.
|
||||
func (sv SensorValue) Proximity() float32 {
|
||||
if sv.id != SensorProximity {
|
||||
panic("bno08x: Proximity() called on non-proximity sensor type")
|
||||
}
|
||||
return sv.proximity
|
||||
}
|
||||
|
||||
// Temperature returns the temperature reading in °C.
|
||||
// Panics if called on a sensor type other than SensorTemperature.
|
||||
func (sv SensorValue) Temperature() float32 {
|
||||
if sv.id != SensorTemperature {
|
||||
panic("bno08x: Temperature() called on non-temperature sensor type")
|
||||
}
|
||||
return sv.temperature
|
||||
}
|
||||
|
||||
// Activity detection accessor methods
|
||||
|
||||
// TapDetector returns the tap detector data.
|
||||
// Panics if called on a sensor type other than SensorTapDetector.
|
||||
func (sv SensorValue) TapDetector() TapDetector {
|
||||
if sv.id != SensorTapDetector {
|
||||
panic("bno08x: TapDetector() called on wrong sensor type")
|
||||
}
|
||||
return sv.tapDetector
|
||||
}
|
||||
|
||||
// StepCounter returns the step counter value.
|
||||
// Panics if called on a sensor type other than SensorStepCounter.
|
||||
func (sv SensorValue) StepCounter() StepCounter {
|
||||
if sv.id != SensorStepCounter {
|
||||
panic("bno08x: StepCounter() called on wrong sensor type")
|
||||
}
|
||||
return sv.stepCounter
|
||||
}
|
||||
|
||||
// StepDetector returns the step detector data.
|
||||
// Panics if called on a sensor type other than SensorStepDetector.
|
||||
func (sv SensorValue) StepDetector() StepDetector {
|
||||
if sv.id != SensorStepDetector {
|
||||
panic("bno08x: StepDetector() called on wrong sensor type")
|
||||
}
|
||||
return sv.stepDetector
|
||||
}
|
||||
|
||||
// SignificantMotion returns the significant motion data.
|
||||
// Panics if called on a sensor type other than SensorSignificantMotion.
|
||||
func (sv SensorValue) SignificantMotion() SignificantMotion {
|
||||
if sv.id != SensorSignificantMotion {
|
||||
panic("bno08x: SignificantMotion() called on wrong sensor type")
|
||||
}
|
||||
return sv.significantMotion
|
||||
}
|
||||
|
||||
// ShakeDetector returns the shake detector data.
|
||||
// Panics if called on a sensor type other than SensorShakeDetector.
|
||||
func (sv SensorValue) ShakeDetector() ShakeDetector {
|
||||
if sv.id != SensorShakeDetector {
|
||||
panic("bno08x: ShakeDetector() called on wrong sensor type")
|
||||
}
|
||||
return sv.shakeDetector
|
||||
}
|
||||
|
||||
// FlipDetector returns the flip detector data.
|
||||
// Panics if called on a sensor type other than SensorFlipDetector.
|
||||
func (sv SensorValue) FlipDetector() uint16 {
|
||||
if sv.id != SensorFlipDetector {
|
||||
panic("bno08x: FlipDetector() called on wrong sensor type")
|
||||
}
|
||||
return sv.flipDetector
|
||||
}
|
||||
|
||||
// StabilityClassifier returns the stability classifier data.
|
||||
// Panics if called on a sensor type other than SensorStabilityClassifier.
|
||||
func (sv SensorValue) StabilityClassifier() StabilityClassifier {
|
||||
if sv.id != SensorStabilityClassifier {
|
||||
panic("bno08x: StabilityClassifier() called on wrong sensor type")
|
||||
}
|
||||
return sv.stabilityClassifier
|
||||
}
|
||||
|
||||
// StabilityDetector returns the stability detector value.
|
||||
// Panics if called on a sensor type other than SensorStabilityDetector.
|
||||
func (sv SensorValue) StabilityDetector() uint8 {
|
||||
if sv.id != SensorStabilityDetector {
|
||||
panic("bno08x: StabilityDetector() called on wrong sensor type")
|
||||
}
|
||||
return sv.stabilityDetector
|
||||
}
|
||||
|
||||
// ActivityClassifier returns the activity classification data.
|
||||
// Note: This field appears unused in decode.go, keeping for API compatibility.
|
||||
func (sv SensorValue) ActivityClassifier() ActivityClassification {
|
||||
return sv.activityClassifier
|
||||
}
|
||||
|
||||
// PersonalActivityClassifier returns the personal activity classifier data.
|
||||
// Panics if called on a sensor type other than SensorPersonalActivityClassifier.
|
||||
func (sv SensorValue) PersonalActivityClassifier() PersonalActivityClassifier {
|
||||
if sv.id != SensorPersonalActivityClassifier {
|
||||
panic("bno08x: PersonalActivityClassifier() called on wrong sensor type")
|
||||
}
|
||||
return sv.personalActivityClassifier
|
||||
}
|
||||
|
||||
// SleepDetector returns the sleep detector value.
|
||||
// Panics if called on a sensor type other than SensorSleepDetector.
|
||||
func (sv SensorValue) SleepDetector() uint8 {
|
||||
if sv.id != SensorSleepDetector {
|
||||
panic("bno08x: SleepDetector() called on wrong sensor type")
|
||||
}
|
||||
return sv.sleepDetector
|
||||
}
|
||||
|
||||
// TiltDetector returns the tilt detector value.
|
||||
// Panics if called on a sensor type other than SensorTiltDetector.
|
||||
func (sv SensorValue) TiltDetector() uint8 {
|
||||
if sv.id != SensorTiltDetector {
|
||||
panic("bno08x: TiltDetector() called on wrong sensor type")
|
||||
}
|
||||
return sv.tiltDetector
|
||||
}
|
||||
|
||||
// PocketDetector returns the pocket detector value.
|
||||
// Panics if called on a sensor type other than SensorPocketDetector.
|
||||
func (sv SensorValue) PocketDetector() uint8 {
|
||||
if sv.id != SensorPocketDetector {
|
||||
panic("bno08x: PocketDetector() called on wrong sensor type")
|
||||
}
|
||||
return sv.pocketDetector
|
||||
}
|
||||
|
||||
// CircleDetector returns the circle detector value.
|
||||
// Panics if called on a sensor type other than SensorCircleDetector.
|
||||
func (sv SensorValue) CircleDetector() uint8 {
|
||||
if sv.id != SensorCircleDetector {
|
||||
panic("bno08x: CircleDetector() called on wrong sensor type")
|
||||
}
|
||||
return sv.circleDetector
|
||||
}
|
||||
|
||||
// HeartRateMonitor returns the heart rate monitor value.
|
||||
// Panics if called on a sensor type other than SensorHeartRateMonitor.
|
||||
func (sv SensorValue) HeartRateMonitor() uint16 {
|
||||
if sv.id != SensorHeartRateMonitor {
|
||||
panic("bno08x: HeartRateMonitor() called on wrong sensor type")
|
||||
}
|
||||
return sv.heartRateMonitor
|
||||
}
|
||||
+7
-7
@@ -2,22 +2,22 @@
|
||||
package buzzer // import "tinygo.org/x/drivers/buzzer"
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// Device wraps a GPIO connection to a buzzer.
|
||||
type Device struct {
|
||||
pin machine.Pin
|
||||
pin pin.OutputFunc
|
||||
High bool
|
||||
BPM float64
|
||||
}
|
||||
|
||||
// New returns a new buzzer driver given which pin to use
|
||||
func New(pin machine.Pin) Device {
|
||||
func New(pin pin.Output) Device {
|
||||
return Device{
|
||||
pin: pin,
|
||||
pin: pin.Set,
|
||||
High: false,
|
||||
BPM: 96.0,
|
||||
}
|
||||
@@ -25,14 +25,14 @@ func New(pin machine.Pin) Device {
|
||||
|
||||
// On sets the buzzer to a high state.
|
||||
func (l *Device) On() (err error) {
|
||||
l.pin.Set(true)
|
||||
l.pin.High()
|
||||
l.High = true
|
||||
return
|
||||
}
|
||||
|
||||
// Off sets the buzzer to a low state.
|
||||
func (l *Device) Off() (err error) {
|
||||
l.pin.Set(false)
|
||||
l.pin.Low()
|
||||
l.High = false
|
||||
return
|
||||
}
|
||||
|
||||
+288
-36
@@ -5,10 +5,12 @@
|
||||
package ds3231 // import "tinygo.org/x/drivers/ds3231"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/regmap"
|
||||
)
|
||||
|
||||
type Mode uint8
|
||||
@@ -17,6 +19,7 @@ type Mode uint8
|
||||
type Device struct {
|
||||
bus drivers.I2C
|
||||
Address uint16
|
||||
d regmap.Device8I2C
|
||||
}
|
||||
|
||||
// New creates a new DS3231 connection. The I2C bus must already be
|
||||
@@ -24,54 +27,50 @@ type Device struct {
|
||||
//
|
||||
// This function only creates the Device object, it does not touch the device.
|
||||
func New(bus drivers.I2C) Device {
|
||||
return Device{
|
||||
d := Device{
|
||||
bus: bus,
|
||||
Address: Address,
|
||||
}
|
||||
d.Configure()
|
||||
return d
|
||||
}
|
||||
|
||||
// Configure sets up the device for communication
|
||||
func (d *Device) Configure() bool {
|
||||
d.d.SetBus(d.bus, d.Address, binary.BigEndian)
|
||||
return true
|
||||
}
|
||||
|
||||
// IsTimeValid return true/false is the time in the device is valid
|
||||
func (d *Device) IsTimeValid() bool {
|
||||
data := []byte{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_STATUS, data)
|
||||
status, err := d.d.Read8(REG_STATUS)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (data[0] & (1 << OSF)) == 0x00
|
||||
return (status & (1 << OSF)) == 0x00
|
||||
}
|
||||
|
||||
// IsRunning returns if the oscillator is running
|
||||
func (d *Device) IsRunning() bool {
|
||||
data := []uint8{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CONTROL, data)
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (data[0] & (1 << EOSC)) == 0x00
|
||||
return (control & (1 << EOSC)) == 0x00
|
||||
}
|
||||
|
||||
// SetRunning starts the internal oscillator
|
||||
func (d *Device) SetRunning(isRunning bool) error {
|
||||
data := []uint8{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CONTROL, data)
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isRunning {
|
||||
data[0] &^= uint8(1 << EOSC)
|
||||
control &^= uint8(1 << EOSC)
|
||||
} else {
|
||||
data[0] |= 1 << EOSC
|
||||
control |= 1 << EOSC
|
||||
}
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_CONTROL, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return d.d.Write8(REG_CONTROL, control)
|
||||
}
|
||||
|
||||
// SetTime sets the date and time in the DS3231. The DS3231 hardware supports
|
||||
@@ -86,18 +85,16 @@ func (d *Device) SetRunning(isRunning bool) error {
|
||||
// 2100 as a leap year, causing it to increment from 2100-02-28 to 2100-02-29
|
||||
// instead of 2100-03-01.
|
||||
func (d *Device) SetTime(dt time.Time) error {
|
||||
data := []byte{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_STATUS, data)
|
||||
status, err := d.d.Read8(REG_STATUS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data[0] &^= 1 << OSF
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_STATUS, data)
|
||||
if err != nil {
|
||||
status &^= 1 << OSF
|
||||
if err = d.d.Write8(REG_STATUS, status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = make([]uint8, 7)
|
||||
data := make([]uint8, 7)
|
||||
data[0] = uint8ToBCD(uint8(dt.Second()))
|
||||
data[1] = uint8ToBCD(uint8(dt.Minute()))
|
||||
data[2] = uint8ToBCD(uint8(dt.Hour()))
|
||||
@@ -118,21 +115,16 @@ func (d *Device) SetTime(dt time.Time) error {
|
||||
data[5] = uint8ToBCD(uint8(dt.Month()) | centuryFlag)
|
||||
data[6] = uint8ToBCD(year)
|
||||
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_TIMEDATE, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return d.bus.Tx(d.Address, append([]byte{REG_TIMEDATE}, data...), nil)
|
||||
}
|
||||
|
||||
// ReadTime returns the date and time
|
||||
func (d *Device) ReadTime() (dt time.Time, err error) {
|
||||
data := make([]uint8, 7)
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.Address), REG_TIMEDATE, data)
|
||||
if err != nil {
|
||||
if err = d.d.ReadData(REG_TIMEDATE, data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
second := bcdToInt(data[0] & 0x7F)
|
||||
minute := bcdToInt(data[1])
|
||||
hour := hoursBCDToInt(data[2])
|
||||
@@ -150,12 +142,264 @@ func (d *Device) ReadTime() (dt time.Time, err error) {
|
||||
|
||||
// ReadTemperature returns the temperature in millicelsius (mC)
|
||||
func (d *Device) ReadTemperature() (int32, error) {
|
||||
data := make([]uint8, 2)
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_TEMP, data)
|
||||
temp, err := d.d.Read16(REG_TEMP)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return milliCelsius(data[0], data[1]), nil
|
||||
return milliCelsius(temp), nil
|
||||
}
|
||||
|
||||
// GetSqwPinMode returns the current square wave output frequency
|
||||
func (d *Device) GetSqwPinMode() SqwPinMode {
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
if err != nil {
|
||||
return SQW_OFF
|
||||
}
|
||||
|
||||
control &= 0x1C // turn off INTCON
|
||||
if control&0x04 != 0 {
|
||||
return SQW_OFF
|
||||
}
|
||||
|
||||
return SqwPinMode(control)
|
||||
}
|
||||
|
||||
// SetSqwPinMode sets the square wave output mode to the given frequency
|
||||
func (d *Device) SetSqwPinMode(mode SqwPinMode) error {
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
control &^= 0x04 // turn off INTCON
|
||||
control &^= 0x18 // set freq bits to 0
|
||||
|
||||
control |= uint8(mode)
|
||||
|
||||
return d.d.Write8(REG_CONTROL, control)
|
||||
}
|
||||
|
||||
// SetAlarm1 sets alarm1 to the given time and mode
|
||||
func (d *Device) SetAlarm1(dt time.Time, mode Alarm1Mode) error {
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if control&(1<<INTCN) == 0x00 {
|
||||
return errors.New("INTCN has to be disabled")
|
||||
}
|
||||
|
||||
A1M1 := uint8((mode & 0x01) << 7)
|
||||
A1M2 := uint8((mode & 0x02) << 6)
|
||||
A1M3 := uint8((mode & 0x04) << 5)
|
||||
A1M4 := uint8((mode & 0x08) << 4)
|
||||
DY_DT := uint8((mode & 0x10) << 2)
|
||||
|
||||
day := dt.Day()
|
||||
if DY_DT > 0 {
|
||||
day = dowToDS3231(int(dt.Weekday()))
|
||||
}
|
||||
|
||||
alarm1 := uint32(uint8ToBCD(uint8(dt.Second()))|A1M1) << 24
|
||||
alarm1 |= uint32(uint8ToBCD(uint8(dt.Minute()))|A1M2) << 16
|
||||
alarm1 |= uint32(uint8ToBCD(uint8(dt.Hour()))|A1M3) << 8
|
||||
alarm1 |= uint32(uint8ToBCD(uint8(day)) | A1M4 | DY_DT)
|
||||
|
||||
if err := d.d.Write32(REG_ALARMONE, alarm1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
control |= AlarmFlag_Alarm1
|
||||
return d.d.Write8(REG_CONTROL, control)
|
||||
}
|
||||
|
||||
// ReadAlarm1 returns the alarm1 time
|
||||
func (d *Device) ReadAlarm1() (dt time.Time, err error) {
|
||||
data := make([]uint8, 4)
|
||||
if err = d.d.ReadData(REG_ALARMONE, data); err != nil {
|
||||
return
|
||||
}
|
||||
second := bcdToInt(data[0] & 0x7F)
|
||||
minute := bcdToInt(data[1] & 0x7F)
|
||||
hour := hoursBCDToInt(data[2] & 0x3F)
|
||||
|
||||
isDayOfWeek := (data[3] & 0x40) >> 6
|
||||
var day int
|
||||
if isDayOfWeek > 0 {
|
||||
day = bcdToInt(data[3] & 0x0F)
|
||||
} else {
|
||||
day = bcdToInt(data[3] & 0x3F)
|
||||
}
|
||||
|
||||
dt = time.Date(2000, 5, day, hour, minute, second, 0, time.UTC)
|
||||
return
|
||||
}
|
||||
|
||||
// SetAlarm2 sets alarm2 to the given time and mode
|
||||
func (d *Device) SetAlarm2(dt time.Time, mode Alarm2Mode) error {
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if control&(1<<INTCN) == 0x00 {
|
||||
return errors.New("INTCN has to be disabled")
|
||||
}
|
||||
|
||||
A2M2 := uint8((mode & 0x01) << 7)
|
||||
A2M3 := uint8((mode & 0x02) << 6)
|
||||
A2M4 := uint8((mode & 0x04) << 5)
|
||||
DY_DT := uint8((mode & 0x08) << 3)
|
||||
|
||||
day := dt.Day()
|
||||
if DY_DT > 0 {
|
||||
day = dowToDS3231(int(dt.Weekday()))
|
||||
}
|
||||
|
||||
data := make([]uint8, 4)
|
||||
data[0] = uint8ToBCD(uint8(dt.Minute())) | A2M2
|
||||
data[1] = uint8ToBCD(uint8(dt.Hour())) | A2M3
|
||||
data[2] = uint8ToBCD(uint8(day)) | A2M4 | DY_DT
|
||||
if err = d.bus.Tx(d.Address, append([]byte{REG_ALARMTWO}, data...), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
control |= AlarmFlag_Alarm2
|
||||
return d.d.Write8(REG_CONTROL, control)
|
||||
}
|
||||
|
||||
// ReadAlarm2 returns the alarm2 time
|
||||
func (d *Device) ReadAlarm2() (dt time.Time, err error) {
|
||||
data := make([]uint8, 3)
|
||||
if err = d.d.ReadData(REG_ALARMTWO, data); err != nil {
|
||||
return
|
||||
}
|
||||
minute := bcdToInt(data[0] & 0x7F)
|
||||
hour := hoursBCDToInt(data[1] & 0x3F)
|
||||
|
||||
isDayOfWeek := (data[2] & 0x40) >> 6
|
||||
var day int
|
||||
if isDayOfWeek > 0 {
|
||||
day = bcdToInt(data[2] & 0x0F)
|
||||
} else {
|
||||
day = bcdToInt(data[2] & 0x3F)
|
||||
}
|
||||
|
||||
dt = time.Date(2000, 5, day, hour, minute, 0, 0, time.UTC)
|
||||
return
|
||||
}
|
||||
|
||||
// IsEnabledAlarm1 returns true when alarm1 is enabled
|
||||
func (d *Device) IsEnabledAlarm1() bool {
|
||||
return d.isEnabledAlarm(1)
|
||||
}
|
||||
|
||||
// SetEnabledAlarm1 sets the enabled status of alarm1
|
||||
func (d *Device) SetEnabledAlarm1(enable bool) error {
|
||||
if enable {
|
||||
return d.enableAlarm(1)
|
||||
}
|
||||
return d.disableAlarm(1)
|
||||
}
|
||||
|
||||
// IsEnabledAlarm2 returns true when alarm2 is enabled
|
||||
func (d *Device) IsEnabledAlarm2() bool {
|
||||
return d.isEnabledAlarm(2)
|
||||
}
|
||||
|
||||
// SetEnabledAlarm2 sets the enabled status of alarm2
|
||||
func (d *Device) SetEnabledAlarm2(enable bool) error {
|
||||
if enable {
|
||||
return d.enableAlarm(2)
|
||||
}
|
||||
return d.disableAlarm(2)
|
||||
}
|
||||
|
||||
// ClearAlarm1 clears status of alarm1
|
||||
func (d *Device) ClearAlarm1() error {
|
||||
return d.clearAlarm(1)
|
||||
}
|
||||
|
||||
// ClearAlarm2 clears status of alarm2
|
||||
func (d *Device) ClearAlarm2() error {
|
||||
return d.clearAlarm(2)
|
||||
}
|
||||
|
||||
// IsAlarm1Fired returns true when alarm1 is firing
|
||||
func (d *Device) IsAlarm1Fired() bool {
|
||||
return d.isAlarmFired(1)
|
||||
}
|
||||
|
||||
// IsAlarm2Fired returns true when alarm2 is firing
|
||||
func (d *Device) IsAlarm2Fired() bool {
|
||||
return d.isAlarmFired(2)
|
||||
}
|
||||
|
||||
// SetEnabled32K sets the enabled status of the 32KHz output
|
||||
func (d *Device) SetEnabled32K(enable bool) error {
|
||||
status, err := d.d.Read8(REG_STATUS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if enable {
|
||||
status |= 1 << EN32KHZ
|
||||
} else {
|
||||
status &^= 1 << EN32KHZ
|
||||
}
|
||||
|
||||
return d.d.Write8(REG_STATUS, status)
|
||||
}
|
||||
|
||||
// IsEnabled32K returns true when the 32KHz output is enabled
|
||||
func (d *Device) IsEnabled32K() bool {
|
||||
status, err := d.d.Read8(REG_STATUS)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (status & (1 << EN32KHZ)) != 0x00
|
||||
}
|
||||
|
||||
func (d *Device) disableAlarm(alarm_num uint8) error {
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
control &^= (1 << (alarm_num - 1))
|
||||
return d.d.Write8(REG_CONTROL, control)
|
||||
}
|
||||
|
||||
func (d *Device) enableAlarm(alarm_num uint8) error {
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
control |= (1 << (alarm_num - 1))
|
||||
return d.d.Write8(REG_CONTROL, control)
|
||||
}
|
||||
|
||||
func (d *Device) isEnabledAlarm(alarm_num uint8) bool {
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (control & (1 << (alarm_num - 1))) != 0x00
|
||||
}
|
||||
|
||||
func (d *Device) clearAlarm(alarm_num uint8) error {
|
||||
status, err := d.d.Read8(REG_STATUS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status &^= (1 << (alarm_num - 1))
|
||||
return d.d.Write8(REG_STATUS, status)
|
||||
}
|
||||
|
||||
func (d *Device) isAlarmFired(alarm_num uint8) bool {
|
||||
status, err := d.d.Read8(REG_STATUS)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (status & (1 << (alarm_num - 1))) != 0x00
|
||||
}
|
||||
|
||||
// milliCelsius converts the raw temperature bytes (msb and lsb) from the DS3231
|
||||
@@ -172,8 +416,8 @@ func (d *Device) ReadTemperature() (int32, error) {
|
||||
// 16-bit signed integer in units of centi Celsius (1/100 deg C) with no loss of
|
||||
// precision or dynamic range. But for backwards compatibility, let's instead
|
||||
// convert this into a 32-bit signed integer in units of milli Celsius.
|
||||
func milliCelsius(msb uint8, lsb uint8) int32 {
|
||||
t256 := int16(uint16(msb)<<8 | uint16(lsb))
|
||||
func milliCelsius(tempBytes uint16) int32 {
|
||||
t256 := int16(uint16(tempBytes>>8)<<8 | uint16(tempBytes&0xFF))
|
||||
t1000 := int32(t256) / 64 * 250
|
||||
return t1000
|
||||
}
|
||||
@@ -200,3 +444,11 @@ func hoursBCDToInt(value uint8) (hour int) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// dowToDS3231 converts the day of the week to internal DS3231 format
|
||||
func dowToDS3231(d int) int {
|
||||
if d == 0 {
|
||||
return 7
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
+13
-13
@@ -5,71 +5,71 @@ import (
|
||||
)
|
||||
|
||||
func TestPositiveMilliCelsius(t *testing.T) {
|
||||
t1000 := milliCelsius(0, 0)
|
||||
t1000 := milliCelsius(0)
|
||||
if t1000 != 0 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0, 0b01000000)
|
||||
t1000 = milliCelsius(0b0000000001000000)
|
||||
if t1000 != 250 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0, 0b10000000)
|
||||
t1000 = milliCelsius(0b0000000010000000)
|
||||
if t1000 != 500 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0, 0b11000000)
|
||||
t1000 = milliCelsius(0b0000000011000000)
|
||||
if t1000 != 750 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(1, 0b00000000)
|
||||
t1000 = milliCelsius(0b0000000100000000)
|
||||
if t1000 != 1000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(2, 0b00000000)
|
||||
t1000 = milliCelsius(0b0000001000000000)
|
||||
if t1000 != 2000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
// highest temperature is 127.750C
|
||||
t1000 = milliCelsius(0x7f, 0b11000000)
|
||||
t1000 = milliCelsius(0b0111111111000000)
|
||||
if t1000 != 127750 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNegativeMilliCelsius(t *testing.T) {
|
||||
t1000 := milliCelsius(0xff, 0b11000000)
|
||||
t1000 := milliCelsius(0b1111111111000000)
|
||||
if t1000 != -250 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0xff, 0b10000000)
|
||||
t1000 = milliCelsius(0b1111111110000000)
|
||||
if t1000 != -500 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0xff, 0b01000000)
|
||||
t1000 = milliCelsius(0b1111111101000000)
|
||||
if t1000 != -750 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0xff, 0b00000000)
|
||||
t1000 = milliCelsius(0b1111111100000000)
|
||||
if t1000 != -1000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0xfe, 0b00000000)
|
||||
t1000 = milliCelsius(0b1111111000000000)
|
||||
if t1000 != -2000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
// lowest temperature is -128.000C
|
||||
t1000 = milliCelsius(0x80, 0b00000000)
|
||||
t1000 = milliCelsius(0b1000000000000000)
|
||||
if t1000 != -128000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
@@ -46,3 +46,52 @@ const (
|
||||
AlarmTwo Mode = 4
|
||||
ModeAlarmBoth Mode = 5
|
||||
)
|
||||
|
||||
// SQW Pin Modes
|
||||
type SqwPinMode uint8
|
||||
|
||||
const (
|
||||
SQW_OFF SqwPinMode = 0x1C
|
||||
SQW_1HZ SqwPinMode = 0x00
|
||||
SQW_1KHZ SqwPinMode = 0x08
|
||||
SQW_4KHZ SqwPinMode = 0x10
|
||||
SQW_8KHZ SqwPinMode = 0x18
|
||||
)
|
||||
|
||||
// Alarm1 Modes define which parts of the set alarm time has to match the current timestamp of the clock device for
|
||||
// alarm1 to fire
|
||||
type Alarm1Mode uint8
|
||||
|
||||
const (
|
||||
// Alarm1 fires every second
|
||||
A1_PER_SECOND Alarm1Mode = 0x0F
|
||||
// Alarm1 fires when the seconds match
|
||||
A1_SECOND Alarm1Mode = 0x0E
|
||||
// Alarm1 fires when both seconds and minutes match
|
||||
A1_MINUTE Alarm1Mode = 0x0C
|
||||
// Alarm1 fires when seconds, minutes and hours match
|
||||
A1_HOUR Alarm1Mode = 0x08
|
||||
// Alarm1 fires when seconds, minutes, hours and the day of the month match
|
||||
A1_DATE Alarm1Mode = 0x00
|
||||
// Alarm1 fires when seconds, minutes, hours and the day of the week match
|
||||
A1_DAY Alarm1Mode = 0x10
|
||||
)
|
||||
|
||||
// Alarm2 Modes define which parts of the set alarm time has to match the current timestamp of the clock device for
|
||||
// alarm2 to fire.
|
||||
//
|
||||
// Alarm2 only supports matching down to the minute unlike alarm1 which supports matching down to the second.
|
||||
type Alarm2Mode uint8
|
||||
|
||||
const (
|
||||
// Alarm2 fires every minute
|
||||
A2_PER_MINUTE Alarm2Mode = 0x07
|
||||
// Alarm2 fires when the minutes match
|
||||
A2_MINUTE Alarm2Mode = 0x06
|
||||
// Alarm2 fires when both minutes and hours match
|
||||
A2_HOUR Alarm2Mode = 0x04
|
||||
// Alarm2 fires when minutes, hours and the day of the month match
|
||||
A2_DATE Alarm2Mode = 0x00
|
||||
// Alarm2 fires when minutes, hours and the day of the week match
|
||||
A2_DAY Alarm2Mode = 0x08
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build tinygo && (rp2040 || stm32 || k210 || esp32c3 || nrf || sam || (avr && (atmega328p || atmega328pb)))
|
||||
//go:build tinygo && (rp2040 || rp2350 || stm32 || k210 || esp32c3 || nrf || sam || (avr && (atmega328p || atmega328pb)))
|
||||
|
||||
// Implementation based on:
|
||||
// https://gist.github.com/aykevl/3fc1683ed77bb0a9c07559dfe857304a
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
// Package ens160 provides a driver for the ScioSense ENS160 digital gas sensor.
|
||||
//
|
||||
// Datasheet: https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdf
|
||||
package ens160
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 30 * time.Millisecond
|
||||
shortTimeout = 1 * time.Millisecond
|
||||
)
|
||||
|
||||
// Conversion constants for environment data compensation.
|
||||
const (
|
||||
kelvinOffsetMilli = 273150 // 273.15 K in milli-units
|
||||
tempRawFactor = 64 // As per datasheet for TEMP_IN
|
||||
humRawFactor = 512 // As per datasheet for RH_IN
|
||||
milliFactor = 1000 // For converting from milli-units
|
||||
roundingTerm = milliFactor / 2 // For rounding before integer division
|
||||
)
|
||||
|
||||
// validityStrings provides human-readable descriptions for validity flags.
|
||||
var validityStrings = [...]string{
|
||||
ValidityNormalOperation: "normal operation",
|
||||
ValidityWarmUpPhase: "warm-up phase, wait ~3 minutes for valid data",
|
||||
ValidityInitialStartUpPhase: "initial start-up phase, wait ~1 hour for valid data",
|
||||
ValidityInvalidOutput: "invalid output",
|
||||
}
|
||||
|
||||
// Device wraps an I2C connection to an ENS160 device.
|
||||
type Device struct {
|
||||
bus drivers.I2C // I²C implementation
|
||||
addr uint16 // 7‑bit bus address, promoted to uint16 per drivers.I2C
|
||||
|
||||
// shadow registers / last measurements
|
||||
lastTvocPPB uint16
|
||||
lastEco2PPM uint16
|
||||
lastAqiUBA uint8
|
||||
lastValidity uint8 // Store the latest validity status
|
||||
|
||||
// pre‑allocated buffers
|
||||
wbuf [5]byte // longest write: reg + 4 bytes (TEMP+RH)
|
||||
rbuf [5]byte // longest read: DATA burst (5 bytes)
|
||||
}
|
||||
|
||||
// New returns a new ENS160 driver.
|
||||
func New(bus drivers.I2C, addr uint16) *Device {
|
||||
if addr == 0 {
|
||||
addr = DefaultAddress
|
||||
}
|
||||
return &Device{
|
||||
bus: bus,
|
||||
addr: addr,
|
||||
lastValidity: ValidityInvalidOutput,
|
||||
}
|
||||
}
|
||||
|
||||
// Connected returns whether a ENS160 has been found.
|
||||
func (d *Device) Connected() bool {
|
||||
d.wbuf[0] = regPartID
|
||||
err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:2])
|
||||
return err == nil && d.rbuf[0] == LowPartID && d.rbuf[1] == HighPartID
|
||||
}
|
||||
|
||||
// Configure sets up the device for reading.
|
||||
func (d *Device) Configure() error {
|
||||
// 1. Soft-reset. The device will automatically enter IDLE mode.
|
||||
if err := d.write1(regOpMode, ModeReset); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(defaultTimeout)
|
||||
|
||||
// 2. Clear GPR registers, then go to STANDARD mode.
|
||||
if err := d.write1(regCommand, cmdClrGPR); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(defaultTimeout)
|
||||
|
||||
if err := d.write1(regOpMode, ModeStandard); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(defaultTimeout)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateTempRaw converts temperature from milli-degrees Celsius to the sensor's raw format.
|
||||
func calculateTempRaw(tempMilliC int32) uint16 {
|
||||
// Clip temperature
|
||||
const (
|
||||
minC = -40 * 1000
|
||||
maxC = 85 * 1000
|
||||
)
|
||||
if tempMilliC < minC {
|
||||
tempMilliC = minC
|
||||
} else if tempMilliC > maxC {
|
||||
tempMilliC = maxC
|
||||
}
|
||||
|
||||
// Integer fixed-point conversion to format required by the sensor.
|
||||
// Formula from datasheet: T_IN = (T_ambient_C + 273.15) * 64
|
||||
return uint16((((tempMilliC + kelvinOffsetMilli) * tempRawFactor) + roundingTerm) / milliFactor)
|
||||
}
|
||||
|
||||
// calculateHumRaw converts relative humidity from milli-percent to the sensor's raw format.
|
||||
func calculateHumRaw(rhMilliPct int32) uint16 {
|
||||
// Clip humidity
|
||||
if rhMilliPct < 0 {
|
||||
rhMilliPct = 0
|
||||
} else if rhMilliPct > 100*1000 {
|
||||
rhMilliPct = 100 * 1000
|
||||
}
|
||||
|
||||
// Integer fixed-point conversion to format required by the sensor.
|
||||
// Formula from datasheet: RH_IN = (RH_ambient_% * 512)
|
||||
return uint16(((rhMilliPct * humRawFactor) + roundingTerm) / milliFactor)
|
||||
}
|
||||
|
||||
// SetEnvDataMilli sets the ambient temperature and humidity for compensation.
|
||||
//
|
||||
// tempMilliC is the temperature in milli-degrees Celsius.
|
||||
// rhMilliPct is the relative humidity in milli-percent.
|
||||
func (d *Device) SetEnvDataMilli(tempMilliC, rhMilliPct int32) error {
|
||||
tempRaw := calculateTempRaw(tempMilliC)
|
||||
humRaw := calculateHumRaw(rhMilliPct)
|
||||
|
||||
d.wbuf[0] = regTempIn // start address (auto‑increment)
|
||||
binary.LittleEndian.PutUint16(d.wbuf[1:3], tempRaw)
|
||||
binary.LittleEndian.PutUint16(d.wbuf[3:5], humRaw)
|
||||
|
||||
return d.bus.Tx(d.addr, d.wbuf[:5], nil)
|
||||
}
|
||||
|
||||
// Update refreshes the concentration measurements.
|
||||
func (d *Device) Update(which drivers.Measurement) error {
|
||||
if which&drivers.Concentration == 0 {
|
||||
return nil // nothing requested
|
||||
}
|
||||
|
||||
const maxTries = 1000
|
||||
var (
|
||||
status uint8
|
||||
validity uint8
|
||||
)
|
||||
var gotData bool
|
||||
|
||||
// Poll DEVICE_STATUS until NEWDAT or timeout
|
||||
for range maxTries {
|
||||
var err error
|
||||
status, err = d.read1(regStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status&statusSTATER != 0 {
|
||||
return errors.New("ENS160: error (STATER set)")
|
||||
}
|
||||
validity = (status & statusValidityMask) >> statusValidityShift
|
||||
|
||||
if status&statusNEWDAT != 0 {
|
||||
gotData = true
|
||||
break // Always break when data available
|
||||
}
|
||||
time.Sleep(shortTimeout)
|
||||
}
|
||||
if !gotData {
|
||||
return errors.New("ENS160: timeout waiting for NEWDAT")
|
||||
}
|
||||
|
||||
// Burst-read data regardless of validity state
|
||||
d.wbuf[0] = regAQI
|
||||
if err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:5]); err != nil {
|
||||
return errors.New("ENS160: burst read failed")
|
||||
}
|
||||
|
||||
d.lastAqiUBA = d.rbuf[0]
|
||||
d.lastTvocPPB = binary.LittleEndian.Uint16(d.rbuf[1:3])
|
||||
d.lastEco2PPM = binary.LittleEndian.Uint16(d.rbuf[3:5])
|
||||
d.lastValidity = validity // Store the validity status
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TVOC returns the last total‑VOC concentration in parts‑per‑billion.
|
||||
func (d *Device) TVOC() uint16 { return d.lastTvocPPB }
|
||||
|
||||
// ECO2 returns the last equivalent CO₂ concentration in parts‑per‑million.
|
||||
func (d *Device) ECO2() uint16 { return d.lastEco2PPM }
|
||||
|
||||
// AQI returns the last Air‑Quality Index according to UBA (1–5).
|
||||
func (d *Device) AQI() uint8 { return d.lastAqiUBA }
|
||||
|
||||
// Validity returns the current operating state of the sensor.
|
||||
func (d *Device) Validity() uint8 {
|
||||
return d.lastValidity
|
||||
}
|
||||
|
||||
// ValidityString returns a human-readable string describing the current validity status.
|
||||
func (d *Device) ValidityString() string {
|
||||
if int(d.lastValidity) < len(validityStrings) {
|
||||
return validityStrings[d.lastValidity]
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// write1 writes a single byte to a register.
|
||||
func (d *Device) write1(reg, val uint8) error {
|
||||
d.wbuf[0] = reg
|
||||
d.wbuf[1] = val
|
||||
return d.bus.Tx(d.addr, d.wbuf[:2], nil)
|
||||
}
|
||||
|
||||
// read1 reads a single byte from a register.
|
||||
func (d *Device) read1(reg uint8) (uint8, error) {
|
||||
d.wbuf[0] = reg
|
||||
if err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:1]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return d.rbuf[0], nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package ens160
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCalculateTempRaw(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tempMilliC int32
|
||||
expectedRaw uint16
|
||||
}{
|
||||
{"25°C", 25000, 19082},
|
||||
{"-10.5°C", -10500, 16810},
|
||||
{"Min temp", -40000, 14922},
|
||||
{"Below min", -50000, 14922},
|
||||
{"Max temp", 85000, 22922},
|
||||
{"Above max", 90000, 22922},
|
||||
{"Zero", 0, 17482},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
raw := calculateTempRaw(tc.tempMilliC)
|
||||
if raw != tc.expectedRaw {
|
||||
t.Errorf("expected %d, got %d", tc.expectedRaw, raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateHumRaw(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
rhMilliPct int32
|
||||
expectedRaw uint16
|
||||
}{
|
||||
{"50%", 50000, 25600},
|
||||
{"0%", 0, 0},
|
||||
{"100%", 100000, 51200},
|
||||
{"Below 0%", -10000, 0},
|
||||
{"Above 100%", 110000, 51200},
|
||||
{"33.3%", 33300, 17050},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
raw := calculateHumRaw(tc.rhMilliPct)
|
||||
if raw != tc.expectedRaw {
|
||||
t.Errorf("expected %d, got %d", tc.expectedRaw, raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package ens160
|
||||
|
||||
// DefaultAddress is the default I2C address for the ENS160 when the ADDR pin is
|
||||
// connected to high (3.3V). When connected to low (GND), the address is 0x52.
|
||||
const DefaultAddress = 0x53
|
||||
|
||||
// Registers
|
||||
const (
|
||||
regPartID = 0x00
|
||||
regOpMode = 0x10
|
||||
regConfig = 0x11
|
||||
regCommand = 0x12
|
||||
regTempIn = 0x13
|
||||
regRhIn = 0x15
|
||||
regStatus = 0x20
|
||||
regAQI = 0x21
|
||||
regTVOC = 0x22
|
||||
regECO2 = 0x24
|
||||
regDataT = 0x30
|
||||
regDataRH = 0x32
|
||||
regMISR = 0x38
|
||||
regGPRWrite = 0x40
|
||||
regGPRRead = 0x48
|
||||
)
|
||||
|
||||
// Operating modes
|
||||
const (
|
||||
ModeDeepSleep = 0x00
|
||||
ModeIdle = 0x01
|
||||
ModeStandard = 0x02
|
||||
ModeReset = 0xF0
|
||||
)
|
||||
|
||||
// Status register bits
|
||||
const (
|
||||
statusSTATAS = 1 << 7
|
||||
statusSTATER = 1 << 6
|
||||
|
||||
statusValidityMask = 0x0C
|
||||
statusValidityShift = 2
|
||||
|
||||
statusNEWDAT = 1 << 1
|
||||
statusNEWGPR = 1 << 0
|
||||
)
|
||||
|
||||
// Validity flags
|
||||
const (
|
||||
ValidityNormalOperation = 0x00
|
||||
ValidityWarmUpPhase = 0x01 // need ~3 minutes until valid data
|
||||
ValidityInitialStartUpPhase = 0x02 // need ~1 hour until valid data
|
||||
ValidityInvalidOutput = 0x03
|
||||
)
|
||||
|
||||
// Commands
|
||||
const (
|
||||
cmdNOP = 0x00
|
||||
cmdGetAppVer = 0x0E
|
||||
cmdClrGPR = 0xCC
|
||||
)
|
||||
|
||||
// Part IDs
|
||||
const (
|
||||
LowPartID = 0x60
|
||||
HighPartID = 0x01
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
// Package main provides a basic example of using the BNO08x driver
|
||||
// to read rotation vector (quaternion) data from the sensor.
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/bno08x"
|
||||
)
|
||||
|
||||
func main() {
|
||||
time.Sleep(2 * time.Second) // Wait for sensor to power up
|
||||
// Initialize I2C bus
|
||||
i2c := machine.I2C0
|
||||
err := i2c.Configure(machine.I2CConfig{
|
||||
Frequency: 400 * machine.KHz,
|
||||
})
|
||||
if err != nil {
|
||||
println("Failed to configure I2C:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
println("Initializing BNO08x sensor...")
|
||||
|
||||
// Create and configure sensor using I2C
|
||||
sensor := bno08x.NewI2C(i2c)
|
||||
err = sensor.Configure(bno08x.Config{})
|
||||
if err != nil {
|
||||
println("Failed to configure sensor:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
println("Sensor initialized successfully")
|
||||
|
||||
// Enable Game Rotation Vector reports at 100Hz (10000 microseconds = 10ms interval)
|
||||
// Using Game Rotation Vector (0x08) to match the working channel_debug test
|
||||
err = sensor.EnableReport(bno08x.SensorGameRotationVector, 10000)
|
||||
if err != nil {
|
||||
println("Failed to enable game rotation vector:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
println("Reading rotation vectors...")
|
||||
println("Format: Real I J K Accuracy")
|
||||
|
||||
// Add a delay after enabling reports (Arduino does this)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Main loop - read and display quaternion data
|
||||
for {
|
||||
event, ok := sensor.GetSensorEvent()
|
||||
if ok && (event.ID() == bno08x.SensorRotationVector || event.ID() == bno08x.SensorGameRotationVector) {
|
||||
q := event.Quaternion()
|
||||
if event.ID() == bno08x.SensorRotationVector {
|
||||
println(q.Real, q.I, q.J, q.K, event.QuaternionAccuracy())
|
||||
} else {
|
||||
// GameRotationVector doesn't have accuracy
|
||||
println(q.Real, q.I, q.J, q.K)
|
||||
}
|
||||
}
|
||||
|
||||
// Arduino uses 10ms delay in loop
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Connects to an DS3231 I2C Real Time Clock (RTC) and sets both alarms. It then repeatedly checks
|
||||
// if the alarms are firing and prints out a message if that is the case.
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/ds3231"
|
||||
)
|
||||
|
||||
func main() {
|
||||
machine.I2C0.Configure(machine.I2CConfig{})
|
||||
|
||||
rtc := ds3231.New(machine.I2C0)
|
||||
rtc.Configure()
|
||||
|
||||
valid := rtc.IsTimeValid()
|
||||
if !valid {
|
||||
date := time.Date(2019, 12, 05, 20, 34, 12, 0, time.UTC)
|
||||
rtc.SetTime(date)
|
||||
}
|
||||
|
||||
// Set alarm1 so it triggers when the seconds match 59 => repeats every minute at dd:hh:mm:59
|
||||
if err := rtc.SetAlarm1(time.Date(0, 0, 0, 0, 0, 59, 0, time.UTC), ds3231.A1_SECOND); err != nil {
|
||||
println("Error while setting Alarm1")
|
||||
}
|
||||
if err := rtc.SetEnabledAlarm1(true); err != nil {
|
||||
println("Error while enabling Alarm1")
|
||||
}
|
||||
|
||||
// Set alarm2 so it triggers when the minutes match 35 => repeats every hour at dd:hh:35:ss
|
||||
if err := rtc.SetAlarm2(time.Date(0, 0, 0, 0, 35, 0, 0, time.UTC), ds3231.A2_MINUTE); err != nil {
|
||||
println("Error while setting Alarm2")
|
||||
}
|
||||
if err := rtc.SetEnabledAlarm2(true); err != nil {
|
||||
println("Error while enabling Alarm2")
|
||||
}
|
||||
|
||||
running := rtc.IsRunning()
|
||||
if !running {
|
||||
err := rtc.SetRunning(true)
|
||||
if err != nil {
|
||||
println("Error configuring RTC")
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
dt, err := rtc.ReadTime()
|
||||
if err != nil {
|
||||
println("Error reading date:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
a1 := rtc.IsAlarm1Fired()
|
||||
a2 := rtc.IsAlarm2Fired()
|
||||
|
||||
println(dt.Format(time.DateTime), "A1:", a1, "A2:", a2)
|
||||
|
||||
if a1 {
|
||||
if err := rtc.ClearAlarm1(); err != nil {
|
||||
println("Error while clearing alarm1")
|
||||
}
|
||||
}
|
||||
if a2 {
|
||||
if err := rtc.ClearAlarm2(); err != nil {
|
||||
println("Error while clearing alarm2")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@ package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
|
||||
"tinygo.org/x/drivers/ds3231"
|
||||
)
|
||||
|
||||
@@ -26,19 +25,19 @@ func main() {
|
||||
if !running {
|
||||
err := rtc.SetRunning(true)
|
||||
if err != nil {
|
||||
fmt.Println("Error configuring RTC")
|
||||
println("Error configuring RTC")
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
dt, err := rtc.ReadTime()
|
||||
if err != nil {
|
||||
fmt.Println("Error reading date:", err)
|
||||
println("Error reading date:", err)
|
||||
} else {
|
||||
fmt.Printf("Date: %d/%s/%02d %02d:%02d:%02d \r\n", dt.Year(), dt.Month(), dt.Day(), dt.Hour(), dt.Minute(), dt.Second())
|
||||
println(dt.Format(time.DateTime))
|
||||
}
|
||||
temp, _ := rtc.ReadTemperature()
|
||||
fmt.Printf("Temperature: %.2f °C \r\n", float32(temp)/1000)
|
||||
println("Temperature:", strconv.FormatFloat(float64(temp)/1000, 'f', -1, 32), "°C")
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// This example demonstrates ENS160 usage.
|
||||
//
|
||||
// Wiring:
|
||||
// - VCC to 3.3V, GND to ground
|
||||
// - SDA to board SDA, SCL to board SCL
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/ens160"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := machine.I2C0.Configure(machine.I2CConfig{
|
||||
Frequency: 400 * machine.KHz,
|
||||
})
|
||||
if err != nil {
|
||||
println("Failed to configure I2C:", err)
|
||||
}
|
||||
|
||||
dev := ens160.New(machine.I2C0, ens160.DefaultAddress)
|
||||
|
||||
connected := dev.Connected()
|
||||
if !connected {
|
||||
println("ENS160 not detected")
|
||||
return
|
||||
}
|
||||
println("ENS160 detected")
|
||||
|
||||
if err := dev.Configure(); err != nil {
|
||||
println("Failed to configure ENS160:", err)
|
||||
}
|
||||
|
||||
for {
|
||||
err := dev.Update(drivers.Concentration)
|
||||
if err != nil {
|
||||
println("Error reading ENS160: %v\n", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
println(
|
||||
"AQI:", dev.AQI(),
|
||||
"TVOC:", dev.TVOC(),
|
||||
"eCO2:", dev.ECO2(),
|
||||
"Validity:", dev.ValidityString(),
|
||||
)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func main() {
|
||||
console_example.RunFor(
|
||||
flash.NewSPI(
|
||||
&machine.SPI1,
|
||||
machine.SPI1,
|
||||
machine.SPI1_SDO_PIN,
|
||||
machine.SPI1_SDI_PIN,
|
||||
machine.SPI1_SCK_PIN,
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func main() {
|
||||
println("GPS I2C Example")
|
||||
machine.I2C0.Configure(machine.I2CConfig{})
|
||||
ublox := gps.NewI2C(machine.I2C0)
|
||||
ublox := gps.NewI2CWithAddress(machine.I2C0, gps.UBLOX_I2C_ADDRESS)
|
||||
parser := gps.NewParser()
|
||||
var fix gps.Fix
|
||||
for {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
println("GPS UART Example")
|
||||
machine.UART1.Configure(machine.UARTConfig{BaudRate: 9600})
|
||||
ublox := gps.NewUART(machine.UART1)
|
||||
parser := gps.NewParser()
|
||||
@@ -16,14 +15,24 @@ func main() {
|
||||
for {
|
||||
s, err := ublox.NextSentence()
|
||||
if err != nil {
|
||||
println(err)
|
||||
continue
|
||||
switch err {
|
||||
case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength:
|
||||
continue
|
||||
default:
|
||||
println("sentence error:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fix, err = parser.Parse(s)
|
||||
if err != nil {
|
||||
println(err)
|
||||
continue
|
||||
switch err {
|
||||
case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength:
|
||||
continue
|
||||
default:
|
||||
println("parse error:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if fix.Valid {
|
||||
print(fix.Time.Format("15:04:05"))
|
||||
@@ -43,7 +52,10 @@ func main() {
|
||||
}
|
||||
println()
|
||||
} else {
|
||||
println("No fix")
|
||||
if fix.Type == gps.GSV {
|
||||
// GSV sentence provides satellite count even if no fix yet
|
||||
println(fix.Satellites, "satellites visible")
|
||||
}
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/honeyhsc"
|
||||
)
|
||||
|
||||
// Data taken from https://github.com/rodan/honeywell_hsc_ssc_i2c/blob/master/hsc_ssc_i2c.cpp
|
||||
// these defaults are valid for the HSCMRNN030PA2A3 chip
|
||||
const (
|
||||
i2cAddress = 0x28
|
||||
// 10%
|
||||
outputMinimum = 0x666
|
||||
// 90% of 2^14 - 1
|
||||
outputMax = 0x399A
|
||||
// min is 0 for sensors that give absolute values
|
||||
pressureMin = 0
|
||||
// 30psi (and we want results in millipascals)
|
||||
// pressureMax = 206842.7
|
||||
pressureMax = 206843 * 1000
|
||||
)
|
||||
|
||||
func main() {
|
||||
bus := machine.I2C0
|
||||
err := bus.Configure(machine.I2CConfig{
|
||||
Frequency: 400_000, // 100kHz minimum and 400kHz I2C maximum clock. 50 to 800 for SPI.
|
||||
SDA: machine.I2C0_SDA_PIN,
|
||||
SCL: machine.I2C0_SCL_PIN,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
sensor := honeyhsc.NewDevI2C(bus, i2cAddress, outputMinimum, outputMax, pressureMin, pressureMax)
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
const measuremask = drivers.Pressure | drivers.Temperature
|
||||
err := sensor.Update(measuremask)
|
||||
if err != nil {
|
||||
println("error updating measurements:", err.Error())
|
||||
continue
|
||||
}
|
||||
P := sensor.Pressure()
|
||||
T := sensor.Temperature()
|
||||
println("pressure:", P, "temperature:", T)
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -14,9 +14,18 @@ func main() {
|
||||
i2c.Configure(machine.I2CConfig{SCL: machine.SCL1_PIN, SDA: machine.SDA1_PIN})
|
||||
|
||||
accel := lis3dh.New(i2c)
|
||||
accel.Address = lis3dh.Address1 // address on the Circuit Playground Express
|
||||
accel.Configure()
|
||||
accel.SetRange(lis3dh.RANGE_2_G)
|
||||
err := accel.Configure(lis3dh.Config{
|
||||
Address: lis3dh.Address1, // address on the Circuit Playground Express
|
||||
})
|
||||
for err != nil {
|
||||
println("could not configure LIS3DH:", err)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
err = accel.SetRange(lis3dh.RANGE_2_G)
|
||||
for err != nil {
|
||||
println("could not set acceleration range:", err)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
println(accel.Connected())
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/lsm303dlhc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// LSM303DLHC is connected to the I2C0 bus on Adafruit Feather M4 via pins: 20(SDA) and 21(SCL).
|
||||
machine.I2C0.Configure(machine.I2CConfig{})
|
||||
|
||||
sensor := lsm303dlhc.New(machine.I2C0)
|
||||
//default settings
|
||||
err := sensor.Configure(lsm303dlhc.Configuration{
|
||||
AccelPowerMode: lsm303dlhc.ACCEL_POWER_NORMAL,
|
||||
AccelRange: lsm303dlhc.ACCEL_RANGE_2G,
|
||||
AccelDataRate: lsm303dlhc.ACCEL_DATARATE_100HZ,
|
||||
MagPowerMode: lsm303dlhc.MAG_POWER_NORMAL,
|
||||
MagSystemMode: lsm303dlhc.MAG_SYSTEM_CONTINUOUS,
|
||||
MagDataRate: lsm303dlhc.MAG_DATARATE_10HZ,
|
||||
})
|
||||
if err != nil {
|
||||
for {
|
||||
println("Failed to configure", err.Error())
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
accel_x, accel_y, accel_z, err := sensor.ReadAcceleration()
|
||||
if err != nil {
|
||||
println("Failed to read accel", err.Error())
|
||||
}
|
||||
println("ACCEL_X:", accel_x, " ACCEL_Y:", accel_y, " ACCEL_Z:", accel_z)
|
||||
|
||||
mag_x, mag_y, mag_z, err := sensor.ReadMagneticField()
|
||||
if err != nil {
|
||||
println("Failed to read mag", err.Error())
|
||||
}
|
||||
println("MAG_X:", mag_x, " MAG_Y:", mag_y, " MAG_Z:", mag_z)
|
||||
|
||||
pitch, roll, _ := sensor.ReadPitchRoll()
|
||||
println("Pitch:", float32(pitch), " Roll:", float32(roll))
|
||||
|
||||
heading, _ := sensor.ReadCompass()
|
||||
println("Heading:", float32(heading), "degrees")
|
||||
|
||||
temp, _ := sensor.ReadTemperature()
|
||||
println("Temperature:", float32(temp)/1000, "*C")
|
||||
|
||||
println("\n")
|
||||
time.Sleep(time.Millisecond * 250)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func main() {
|
||||
SDI: machine.SPI0_SDI_PIN,
|
||||
Mode: 0})
|
||||
can := mcp2515.New(spi, csPin)
|
||||
can.Configure()
|
||||
can.Configure(mcp2515.Configuration{})
|
||||
err := can.Begin(mcp2515.CAN500kBps, mcp2515.Clock8MHz)
|
||||
if err != nil {
|
||||
failMessage(err.Error())
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/sd"
|
||||
)
|
||||
|
||||
const (
|
||||
SPI_RX_PIN = machine.GP16
|
||||
SPI_TX_PIN = machine.GP19
|
||||
SPI_SCK_PIN = machine.GP18
|
||||
SPI_CS_PIN = machine.GP15
|
||||
)
|
||||
|
||||
var (
|
||||
spibus = machine.SPI0
|
||||
spicfg = machine.SPIConfig{
|
||||
Frequency: 250000,
|
||||
Mode: 0,
|
||||
SCK: SPI_SCK_PIN,
|
||||
SDO: SPI_TX_PIN,
|
||||
SDI: SPI_RX_PIN,
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
time.Sleep(time.Second)
|
||||
SPI_CS_PIN.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
err := spibus.Configure(spicfg)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
sdcard := sd.NewSPICard(spibus, SPI_CS_PIN.Set)
|
||||
println("start init")
|
||||
err = sdcard.Init()
|
||||
if err != nil {
|
||||
panic("sd card init:" + err.Error())
|
||||
}
|
||||
// After initialization it's safe to increase SPI clock speed.
|
||||
csd := sdcard.CSD()
|
||||
kbps := csd.TransferSpeed().RateKilobits()
|
||||
spicfg.Frequency = uint32(kbps * 1000)
|
||||
err = spibus.Configure(spicfg)
|
||||
|
||||
cid := sdcard.CID()
|
||||
fmt.Printf("name=%s\ncsd=\n%s\n", cid.ProductName(), csd.String())
|
||||
|
||||
bd, err := sd.NewBlockDevice(sdcard, csd.ReadBlockLen(), csd.NumberOfBlocks())
|
||||
if err != nil {
|
||||
panic("block device creation:" + err.Error())
|
||||
}
|
||||
var mc MemChecker
|
||||
|
||||
ok, badBlkIdx, err := mc.MemCheck(bd, 2, 100)
|
||||
if err != nil {
|
||||
panic("memcheck:" + err.Error())
|
||||
}
|
||||
if !ok {
|
||||
println("bad block", badBlkIdx)
|
||||
} else {
|
||||
println("memcheck ok")
|
||||
}
|
||||
}
|
||||
|
||||
type MemChecker struct {
|
||||
rdBuf []byte
|
||||
storeBuf []byte
|
||||
wrBuf []byte
|
||||
}
|
||||
|
||||
func (mc *MemChecker) MemCheck(bd *sd.BlockDevice, blockIdx, numBlocks int64) (memOK bool, badBlockIdx int64, err error) {
|
||||
size := bd.BlockSize() * numBlocks
|
||||
if len(mc.rdBuf) < int(size) {
|
||||
mc.rdBuf = make([]byte, size)
|
||||
mc.wrBuf = make([]byte, size)
|
||||
mc.storeBuf = make([]byte, size)
|
||||
for i := range mc.wrBuf {
|
||||
mc.wrBuf[i] = byte(i)
|
||||
}
|
||||
}
|
||||
// Start by storing the original block contents.
|
||||
_, err = bd.ReadAt(mc.storeBuf, blockIdx)
|
||||
if err != nil {
|
||||
return false, blockIdx, err
|
||||
}
|
||||
|
||||
// Write the test pattern.
|
||||
_, err = bd.WriteAt(mc.wrBuf, blockIdx)
|
||||
if err != nil {
|
||||
return false, blockIdx, err
|
||||
}
|
||||
// Read back the test pattern.
|
||||
_, err = bd.ReadAt(mc.rdBuf, blockIdx)
|
||||
if err != nil {
|
||||
return false, blockIdx, err
|
||||
}
|
||||
for j := 0; j < len(mc.rdBuf); j++ {
|
||||
// Compare the read back data with the test pattern.
|
||||
if mc.rdBuf[j] != mc.wrBuf[j] {
|
||||
badBlock := blockIdx + int64(j)/bd.BlockSize()
|
||||
return false, badBlock, nil
|
||||
}
|
||||
mc.rdBuf[j] = 0
|
||||
}
|
||||
// Leave the card in it's previous state.
|
||||
_, err = bd.WriteAt(mc.storeBuf, blockIdx)
|
||||
return true, -1, nil
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
spi = &machine.SPI0
|
||||
spi = machine.SPI0
|
||||
sckPin = machine.SPI0_SCK_PIN
|
||||
sdoPin = machine.SPI0_SDO_PIN
|
||||
sdiPin = machine.SPI0_SDI_PIN
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
spi = &machine.SPI1
|
||||
spi = machine.SPI1
|
||||
sckPin = machine.SDCARD_SCK_PIN
|
||||
sdoPin = machine.SDCARD_SDO_PIN
|
||||
sdiPin = machine.SDCARD_SDI_PIN
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
spi = &machine.SPI0
|
||||
spi = machine.SPI0
|
||||
sckPin = machine.SPI0_SCK_PIN
|
||||
sdoPin = machine.SPI0_SDO_PIN
|
||||
sdiPin = machine.SPI0_SDI_PIN
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
spi = &machine.SDCARD_SPI
|
||||
spi = machine.SDCARD_SPI
|
||||
sckPin = machine.SDCARD_SCK_PIN
|
||||
sdoPin = machine.SDCARD_SDO_PIN
|
||||
sdiPin = machine.SDCARD_SDI_PIN
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
spi = &machine.SPI0
|
||||
spi = machine.SPI0
|
||||
sckPin = machine.SPI0_SCK_PIN
|
||||
sdoPin = machine.SPI0_SDO_PIN
|
||||
sdiPin = machine.SPI0_SDI_PIN
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
spi = &machine.SPI0
|
||||
spi = machine.SPI0
|
||||
sckPin = machine.SPI0_SCK_PIN
|
||||
sdoPin = machine.SPI0_SDO_PIN
|
||||
sdiPin = machine.SPI0_SDI_PIN
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
spi = &machine.SPI2
|
||||
spi = machine.SPI2
|
||||
sckPin = machine.SCK2
|
||||
sdoPin = machine.SDO2
|
||||
sdiPin = machine.SDI2
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/seesaw"
|
||||
)
|
||||
|
||||
// example reading the position of a rotary encoder (4991) powered by a seesaw
|
||||
// https://learn.adafruit.com/adafruit-i2c-qt-rotary-encoder/arduino
|
||||
func main() {
|
||||
// This assumes you are using an Adafruit QT Py RP2040 for its Stemma QT connector
|
||||
// https://www.adafruit.com/product/4900
|
||||
i2c := machine.I2C1
|
||||
i2c.Configure(machine.I2CConfig{
|
||||
SCL: machine.I2C1_QT_SCL_PIN,
|
||||
SDA: machine.I2C1_QT_SDA_PIN,
|
||||
})
|
||||
|
||||
dev := seesaw.New(i2c)
|
||||
dev.Address = 0x36
|
||||
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
|
||||
pos, err := dev.GetEncoderPosition(0, false)
|
||||
if err != nil {
|
||||
println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
println(pos)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/si5351"
|
||||
)
|
||||
|
||||
// Simple demo of the SI5351 clock generator.
|
||||
// This is like the Arduino library example:
|
||||
// https://github.com/adafruit/Adafruit_Si5351_Library/blob/master/examples/si5351/si5351.ino
|
||||
// Which will configure the chip with:
|
||||
// - PLL A at 900mhz
|
||||
// - PLL B at 616.66667mhz
|
||||
// - Clock 0 at 112.5mhz, using PLL A as a source divided by 8
|
||||
// - Clock 1 at 13.5531mhz, using PLL B as a source divided by 45.5
|
||||
// - Clock 2 at 10.76khz, using PLL B as a source divided by 900 and further divided with an R divider of 64.
|
||||
|
||||
func main() {
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
println("Si5351 Clockgen Test")
|
||||
println()
|
||||
|
||||
// Configure I2C bus
|
||||
machine.I2C0.Configure(machine.I2CConfig{})
|
||||
|
||||
// Create driver instance
|
||||
clockgen := si5351.New(machine.I2C0)
|
||||
|
||||
// Initialize device
|
||||
cnf := si5351.Config{
|
||||
Capacitance: si5351.CrystalLoad10PF,
|
||||
}
|
||||
|
||||
if err := clockgen.Configure(cnf); err != nil {
|
||||
println("Failed to configure Si5351:", err.Error())
|
||||
return
|
||||
}
|
||||
println("Si5351 configured")
|
||||
|
||||
// Now configure the clock outputs.
|
||||
clockgen.SetFrequency(si5351.Clock0, 112_500_000)
|
||||
println("Clock 0: 112.5mhz")
|
||||
|
||||
// Next configure clock 1 for 13.5531mhz (616.6667mhz / 45.5).
|
||||
// This uses fractional division.
|
||||
clockgen.SetFrequency(si5351.Clock1, 13_553_125)
|
||||
println("Clock 1: 13.5531mhz")
|
||||
|
||||
// Finally configure clock 2 to output of 10.706khz.
|
||||
clockgen.SetFrequency(si5351.Clock2, 10_706)
|
||||
println("Clock 2: 10.706khz")
|
||||
|
||||
// After configuring the clocks enable the outputs.
|
||||
clockgen.EnableOutput(si5351.Clock0, true)
|
||||
clockgen.EnableOutput(si5351.Clock1, true)
|
||||
clockgen.EnableOutput(si5351.Clock2, true)
|
||||
println("All outputs enabled")
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
clockgen.EnableOutput(si5351.Clock0, false)
|
||||
clockgen.EnableOutput(si5351.Clock1, false)
|
||||
clockgen.EnableOutput(si5351.Clock2, false)
|
||||
println("All outputs disabled for 5 seconds")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Now turn clock outputs on and off repeatedly
|
||||
on := false
|
||||
for {
|
||||
if on {
|
||||
println("Setting clock outputs off")
|
||||
clockgen.EnableOutput(si5351.Clock0, false)
|
||||
clockgen.EnableOutput(si5351.Clock1, false)
|
||||
clockgen.EnableOutput(si5351.Clock2, false)
|
||||
on = false
|
||||
} else {
|
||||
println("Setting clock outputs on")
|
||||
clockgen.EnableOutput(si5351.Clock0, true)
|
||||
clockgen.EnableOutput(si5351.Clock1, true)
|
||||
clockgen.EnableOutput(si5351.Clock2, true)
|
||||
on = true
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"machine"
|
||||
"math/rand"
|
||||
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
"tinygo.org/x/drivers/ssd1289"
|
||||
)
|
||||
|
||||
@@ -16,7 +17,7 @@ func main() {
|
||||
//consider creating a more efficient bus implementation that uses
|
||||
//your microcontrollers built in "ports"
|
||||
//see rp2040bus.go for an example for the rapsberry pi pico
|
||||
bus := ssd1289.NewPinBus([16]machine.Pin{
|
||||
bus := ssd1289.NewPinBus([16]pin.Output{
|
||||
machine.GP4, //DB0
|
||||
machine.GP5, //DB1
|
||||
machine.GP6, //DB2
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/ssd1306"
|
||||
)
|
||||
|
||||
func main() {
|
||||
machine.I2C0.Configure(machine.I2CConfig{
|
||||
Frequency: machine.TWI_FREQ_400KHZ,
|
||||
})
|
||||
|
||||
display := ssd1306.NewI2C(machine.I2C0)
|
||||
display.Configure(ssd1306.Config{
|
||||
Address: ssd1306.Address_128_32,
|
||||
Width: 128,
|
||||
Height: 32,
|
||||
})
|
||||
|
||||
display.ClearDisplay()
|
||||
|
||||
x := int16(0)
|
||||
y := int16(0)
|
||||
deltaX := int16(1)
|
||||
deltaY := int16(1)
|
||||
for {
|
||||
pixel := display.GetPixel(x, y)
|
||||
c := color.RGBA{255, 255, 255, 255}
|
||||
if pixel {
|
||||
c = color.RGBA{0, 0, 0, 255}
|
||||
}
|
||||
display.SetPixel(x, y, c)
|
||||
display.Display()
|
||||
|
||||
x += deltaX
|
||||
y += deltaY
|
||||
|
||||
if x == 0 || x == 127 {
|
||||
deltaX = -deltaX
|
||||
}
|
||||
|
||||
if y == 0 || y == 31 {
|
||||
deltaY = -deltaY
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// This example shows how to use 128x64 display over I2C
|
||||
// Tested on Seeeduino XIAO Expansion Board https://wiki.seeedstudio.com/Seeeduino-XIAO-Expansion-Board/
|
||||
//
|
||||
// According to manual, I2C address of the display is 0x78, but that's 8-bit address.
|
||||
// TinyGo operates on 7-bit addresses and respective 7-bit address would be 0x3C, which we use below.
|
||||
//
|
||||
// To learn more about different types of I2C addresses, please see following page
|
||||
// https://www.totalphase.com/support/articles/200349176-7-bit-8-bit-and-10-bit-I2C-Slave-Addressing
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/ssd1306"
|
||||
)
|
||||
|
||||
func main() {
|
||||
machine.I2C0.Configure(machine.I2CConfig{
|
||||
Frequency: machine.TWI_FREQ_400KHZ,
|
||||
})
|
||||
|
||||
display := ssd1306.NewI2C(machine.I2C0)
|
||||
display.Configure(ssd1306.Config{
|
||||
Address: 0x3C,
|
||||
Width: 128,
|
||||
Height: 64,
|
||||
})
|
||||
|
||||
display.ClearDisplay()
|
||||
|
||||
x := int16(0)
|
||||
y := int16(0)
|
||||
deltaX := int16(1)
|
||||
deltaY := int16(1)
|
||||
for {
|
||||
pixel := display.GetPixel(x, y)
|
||||
c := color.RGBA{255, 255, 255, 255}
|
||||
if pixel {
|
||||
c = color.RGBA{0, 0, 0, 255}
|
||||
}
|
||||
display.SetPixel(x, y, c)
|
||||
display.Display()
|
||||
|
||||
x += deltaX
|
||||
y += deltaY
|
||||
|
||||
if x == 0 || x == 127 {
|
||||
deltaX = -deltaX
|
||||
}
|
||||
|
||||
if y == 0 || y == 63 {
|
||||
deltaY = -deltaY
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
// This example shows how to use SSD1306 OLED display driver over I2C and SPI.
|
||||
//
|
||||
// Check the `newSSD1306Display()` functions for I2C and SPI initializations.
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"image/color"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
display := newSSD1306Display()
|
||||
display.ClearDisplay()
|
||||
|
||||
w, h := display.Size()
|
||||
x := int16(0)
|
||||
y := int16(0)
|
||||
deltaX := int16(1)
|
||||
deltaY := int16(1)
|
||||
|
||||
traceTime := time.Now().UnixMilli() + 1000
|
||||
frames := 0
|
||||
ms := runtime.MemStats{}
|
||||
|
||||
for {
|
||||
pixel := display.GetPixel(x, y)
|
||||
c := color.RGBA{255, 255, 255, 255}
|
||||
if pixel {
|
||||
c = color.RGBA{0, 0, 0, 255}
|
||||
}
|
||||
display.SetPixel(x, y, c)
|
||||
display.Display()
|
||||
|
||||
x += deltaX
|
||||
y += deltaY
|
||||
|
||||
if x == 0 || x == w-1 {
|
||||
deltaX = -deltaX
|
||||
}
|
||||
|
||||
if y == 0 || y == h-1 {
|
||||
deltaY = -deltaY
|
||||
}
|
||||
|
||||
frames++
|
||||
now := time.Now().UnixMilli()
|
||||
if now >= traceTime {
|
||||
runtime.ReadMemStats(&ms)
|
||||
println("TS", now, "| FPS", frames, "| HeapInuse", ms.HeapInuse)
|
||||
traceTime = now + 1000
|
||||
frames = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//go:build xiao_ble
|
||||
|
||||
// This initializes SSD1306 OLED display driver over I2C.
|
||||
//
|
||||
// Seeed XIAO BLE board + SSD1306 128x32 I2C OLED display.
|
||||
//
|
||||
// Wiring:
|
||||
// - XIAO GND -> OLED GND
|
||||
// - XIAO 3v3 -> OLED VCC
|
||||
// - XIAO D4 (SDA) -> OLED SDA
|
||||
// - XIAO D5 (SCL) -> OLED SCK
|
||||
//
|
||||
// For your case:
|
||||
// - Connect the display to I2C pins on your board.
|
||||
// - Adjust I2C address and display size as needed.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/ssd1306"
|
||||
)
|
||||
|
||||
func newSSD1306Display() *ssd1306.Device {
|
||||
machine.I2C0.Configure(machine.I2CConfig{
|
||||
Frequency: 400 * machine.KHz,
|
||||
SDA: machine.SDA0_PIN,
|
||||
SCL: machine.SCL0_PIN,
|
||||
})
|
||||
display := ssd1306.NewI2C(machine.I2C0)
|
||||
display.Configure(ssd1306.Config{
|
||||
Address: ssd1306.Address_128_32, // or ssd1306.Address
|
||||
Width: 128,
|
||||
Height: 32, // or 64
|
||||
})
|
||||
return display
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//go:build thumby
|
||||
|
||||
// This initializes SSD1306 OLED display driver over SPI.
|
||||
//
|
||||
// Thumby board has a tiny built-in 72x40 display.
|
||||
//
|
||||
// As the display is built-in, no wiring is needed.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/ssd1306"
|
||||
)
|
||||
|
||||
func newSSD1306Display() *ssd1306.Device {
|
||||
machine.SPI0.Configure(machine.SPIConfig{})
|
||||
display := ssd1306.NewSPI(machine.SPI0, machine.THUMBY_DC_PIN, machine.THUMBY_RESET_PIN, machine.THUMBY_CS_PIN)
|
||||
display.Configure(ssd1306.Config{
|
||||
Width: 72,
|
||||
Height: 40,
|
||||
ResetCol: ssd1306.ResetValue{28, 99},
|
||||
ResetPage: ssd1306.ResetValue{0, 5},
|
||||
})
|
||||
return display
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build xiao_rp2040
|
||||
|
||||
// This initializes SSD1306 OLED display driver over SPI.
|
||||
//
|
||||
// Seeed XIAO RP2040 board + SSD1306 128x64 SPI OLED display.
|
||||
//
|
||||
// Wiring:
|
||||
// - XIAO GND -> OLED GND
|
||||
// - XIAO 3v3 -> OLED VCC
|
||||
// - XIAO D8 (SCK) -> OLED D0
|
||||
// - XIAO D10 (SDO) -> OLED D1
|
||||
// - XIAO D4 -> OLED RES
|
||||
// - XIAO D5 -> OLED DC
|
||||
// - XIAO D6 -> OLED CS
|
||||
//
|
||||
// For your case:
|
||||
// - Connect the display to SPI pins on your board.
|
||||
// - Adjust RES, DC and CS pins as needed.
|
||||
// - Adjust SPI frequency as needed.
|
||||
// - Adjust display size as needed.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/ssd1306"
|
||||
)
|
||||
|
||||
func newSSD1306Display() *ssd1306.Device {
|
||||
machine.SPI0.Configure(machine.SPIConfig{
|
||||
Frequency: 50 * machine.MHz,
|
||||
})
|
||||
display := ssd1306.NewSPI(machine.SPI0, machine.D5, machine.D4, machine.D6)
|
||||
display.Configure(ssd1306.Config{
|
||||
Width: 128,
|
||||
Height: 64,
|
||||
})
|
||||
return display
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/ssd1306"
|
||||
)
|
||||
|
||||
func main() {
|
||||
machine.SPI0.Configure(machine.SPIConfig{
|
||||
Frequency: 8000000,
|
||||
})
|
||||
display := ssd1306.NewSPI(machine.SPI0, machine.P8, machine.P7, machine.P9)
|
||||
display.Configure(ssd1306.Config{
|
||||
Width: 128,
|
||||
Height: 64,
|
||||
})
|
||||
|
||||
display.ClearDisplay()
|
||||
|
||||
x := int16(64)
|
||||
y := int16(32)
|
||||
deltaX := int16(1)
|
||||
deltaY := int16(1)
|
||||
for {
|
||||
pixel := display.GetPixel(x, y)
|
||||
c := color.RGBA{255, 255, 255, 255}
|
||||
if pixel {
|
||||
c = color.RGBA{0, 0, 0, 255}
|
||||
}
|
||||
display.SetPixel(x, y, c)
|
||||
display.Display()
|
||||
|
||||
x += deltaX
|
||||
y += deltaY
|
||||
|
||||
if x == 0 || x == 127 {
|
||||
deltaX = -deltaX
|
||||
}
|
||||
|
||||
if y == 0 || y == 63 {
|
||||
deltaY = -deltaY
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// This example using the SSD1306 OLED display over SPI on the Thumby board
|
||||
// A very tiny 72x40 display.
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/ssd1306"
|
||||
)
|
||||
|
||||
func main() {
|
||||
machine.SPI0.Configure(machine.SPIConfig{})
|
||||
display := ssd1306.NewSPI(machine.SPI0, machine.THUMBY_DC_PIN, machine.THUMBY_RESET_PIN, machine.THUMBY_CS_PIN)
|
||||
display.Configure(ssd1306.Config{
|
||||
Width: 72,
|
||||
Height: 40,
|
||||
ResetCol: ssd1306.ResetValue{28, 99},
|
||||
ResetPage: ssd1306.ResetValue{0, 5},
|
||||
})
|
||||
|
||||
display.ClearDisplay()
|
||||
|
||||
x := int16(36)
|
||||
y := int16(20)
|
||||
deltaX := int16(1)
|
||||
deltaY := int16(1)
|
||||
for {
|
||||
pixel := display.GetPixel(x, y)
|
||||
c := color.RGBA{255, 255, 255, 255}
|
||||
if pixel {
|
||||
c = color.RGBA{0, 0, 0, 255}
|
||||
}
|
||||
display.SetPixel(x, y, c)
|
||||
display.Display()
|
||||
|
||||
x += deltaX
|
||||
y += deltaY
|
||||
|
||||
if x == 0 || x == 71 {
|
||||
deltaX = -deltaX
|
||||
}
|
||||
|
||||
if y == 0 || y == 39 {
|
||||
deltaY = -deltaY
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"machine"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/sx128x"
|
||||
)
|
||||
|
||||
var (
|
||||
// pin mapping specific to the lilygo t3s3, change as needed for your board
|
||||
sdoPin = machine.GPIO6
|
||||
sdiPin = machine.GPIO3
|
||||
sckPin = machine.GPIO5
|
||||
nssPin = machine.GPIO7
|
||||
busyPin = machine.GPIO36
|
||||
resetPin = machine.GPIO8
|
||||
dio1Pin = machine.GPIO9
|
||||
)
|
||||
|
||||
func setupPins() {
|
||||
nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
nssPin.Set(true)
|
||||
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Set(true)
|
||||
|
||||
busyPin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupPins()
|
||||
|
||||
spi := machine.SPI0
|
||||
spi.Configure(machine.SPIConfig{
|
||||
Mode: 0,
|
||||
Frequency: 8 * 1e6,
|
||||
SDO: sdoPin,
|
||||
SDI: sdiPin,
|
||||
SCK: sckPin,
|
||||
})
|
||||
|
||||
radio := sx128x.New(
|
||||
spi,
|
||||
nssPin,
|
||||
resetPin,
|
||||
busyPin,
|
||||
)
|
||||
|
||||
radio.WaitWhileBusy(time.Second)
|
||||
SetupLora(radio)
|
||||
|
||||
for {
|
||||
data, err := Rx(radio)
|
||||
if err != nil {
|
||||
println("failed to receive:", err)
|
||||
} else {
|
||||
println("received:", string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetupLora(radio *sx128x.Device) {
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetPacketType(sx128x.PACKET_TYPE_LORA)
|
||||
radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC)
|
||||
|
||||
radio.SetRfFrequency(2400000000) // 2.4Ghz
|
||||
radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7)
|
||||
|
||||
// section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor.
|
||||
radio.WriteRegister(0x925, []byte{0x32})
|
||||
radio.WriteRegister(0x93C, []byte{0x01})
|
||||
|
||||
radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US)
|
||||
radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD)
|
||||
radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424
|
||||
|
||||
}
|
||||
|
||||
func Rx(radio *sx128x.Device) ([]byte, error) {
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetDioIrqParams(sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00)
|
||||
radio.SetBufferBaseAddress(0, 0)
|
||||
radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK)
|
||||
radio.SetRx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s
|
||||
// busy wait for IRQ indication
|
||||
for dio1Pin.Get() == false {
|
||||
runtime.Gosched()
|
||||
}
|
||||
irqStatus, _ := radio.GetIrqStatus()
|
||||
if irqStatus&sx128x.IRQ_RX_DONE_MASK != 0 {
|
||||
payloadLength, bufferOffset, err := radio.GetRxBufferStatus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := radio.ReadBuffer(bufferOffset, payloadLength)
|
||||
return data, nil
|
||||
} else if irqStatus&sx128x.IRQ_RX_TX_TIMEOUT_MASK != 0 {
|
||||
return nil, errors.New("rx timeout")
|
||||
}
|
||||
return nil, errors.New("unexpected IRQ status")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"machine"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/sx128x"
|
||||
)
|
||||
|
||||
var (
|
||||
// pin mapping specific to the lilygo t3s3, change as needed for your board
|
||||
sdoPin = machine.GPIO6
|
||||
sdiPin = machine.GPIO3
|
||||
sckPin = machine.GPIO5
|
||||
nssPin = machine.GPIO7
|
||||
busyPin = machine.GPIO36
|
||||
resetPin = machine.GPIO8
|
||||
dio1Pin = machine.GPIO9
|
||||
)
|
||||
|
||||
func setupPins() {
|
||||
nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
nssPin.Set(true)
|
||||
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Set(true)
|
||||
|
||||
busyPin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupPins()
|
||||
|
||||
spi := machine.SPI0
|
||||
spi.Configure(machine.SPIConfig{
|
||||
Mode: 0,
|
||||
Frequency: 8 * 1e6,
|
||||
SDO: sdoPin,
|
||||
SDI: sdiPin,
|
||||
SCK: sckPin,
|
||||
})
|
||||
|
||||
radio := sx128x.New(
|
||||
spi,
|
||||
nssPin,
|
||||
resetPin,
|
||||
busyPin,
|
||||
)
|
||||
|
||||
radio.WaitWhileBusy(time.Second)
|
||||
SetupLora(radio)
|
||||
|
||||
for {
|
||||
Tx(radio, []byte("Hello, world!"))
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func SetupLora(radio *sx128x.Device) {
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetPacketType(sx128x.PACKET_TYPE_LORA)
|
||||
radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC)
|
||||
|
||||
radio.SetRfFrequency(2400000000) // 2.4Ghz
|
||||
radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7)
|
||||
|
||||
// section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor.
|
||||
radio.WriteRegister(0x925, []byte{0x32})
|
||||
radio.WriteRegister(0x93C, []byte{0x01})
|
||||
|
||||
radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US)
|
||||
radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD)
|
||||
radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424
|
||||
|
||||
}
|
||||
|
||||
func Tx(radio *sx128x.Device, data []byte) error {
|
||||
if len(data) > 255 {
|
||||
return errors.New("data length exceeds maximum of 255 bytes")
|
||||
}
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, uint8(len(data)&0xFF), sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD)
|
||||
radio.SetBufferBaseAddress(0, 0)
|
||||
radio.WriteBuffer(0, data)
|
||||
radio.SetDioIrqParams(sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00)
|
||||
radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK)
|
||||
radio.SetTx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s
|
||||
// busy wait for IRQ indication
|
||||
for dio1Pin.Get() == false {
|
||||
runtime.Gosched()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func main() {
|
||||
//bind csPin to driverAdddress
|
||||
driverAddress := uint8(0) // Let's assume we are working with driver at address 0x01
|
||||
// Step 3. Bind the communication interface to the protocol
|
||||
comm := tmc5160.NewSPIComm(*spi, csPins)
|
||||
comm := tmc5160.NewSPIComm(spi, csPins)
|
||||
// Step 4. Define your stepper like this below
|
||||
//stepper := tmc5160.NewStepper(angle , gearRatio vSupply rCoil , lCoil , iPeak , rSense , mSteps, fclk )
|
||||
stepper := tmc5160.NewDefaultStepper() // Default Stepper should be used only for testing.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"image/color"
|
||||
"math/rand"
|
||||
|
||||
"tinygo.org/x/drivers/unoqmatrix"
|
||||
)
|
||||
|
||||
var on = color.RGBA{255, 255, 255, 255}
|
||||
|
||||
func main() {
|
||||
display := unoqmatrix.NewFromBasePin(machine.PF0)
|
||||
display.ClearDisplay()
|
||||
|
||||
w, h := display.Size()
|
||||
x := int16(0)
|
||||
y := int16(0)
|
||||
deltaX := int16(1)
|
||||
deltaY := int16(1)
|
||||
|
||||
for {
|
||||
pixel := display.GetPixel(x, y)
|
||||
if pixel.R != 0 || pixel.G != 0 || pixel.B != 0 {
|
||||
display.ClearDisplay()
|
||||
x = 1 + int16(rand.Int31n(3))
|
||||
y = 1 + int16(rand.Int31n(3))
|
||||
deltaX = 1
|
||||
deltaY = 1
|
||||
if rand.Int31n(2) == 0 {
|
||||
deltaX = -1
|
||||
}
|
||||
if rand.Int31n(2) == 0 {
|
||||
deltaY = -1
|
||||
}
|
||||
}
|
||||
display.SetPixel(x, y, on)
|
||||
|
||||
x += deltaX
|
||||
y += deltaY
|
||||
|
||||
if x == 0 || x == w-1 {
|
||||
deltaX = -deltaX
|
||||
}
|
||||
|
||||
if y == 0 || y == h-1 {
|
||||
deltaY = -deltaY
|
||||
}
|
||||
|
||||
display.Display()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/netdev"
|
||||
"tinygo.org/x/drivers/w5500"
|
||||
)
|
||||
|
||||
func main() {
|
||||
machine.SPI0.Configure(machine.SPIConfig{
|
||||
Frequency: 33 * machine.MHz,
|
||||
})
|
||||
machine.GPIO17.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
|
||||
eth := w5500.New(machine.SPI0, machine.GPIO17)
|
||||
eth.Configure(w5500.Config{
|
||||
MAC: net.HardwareAddr{0xee, 0xbe, 0xe9, 0xa9, 0xb6, 0x4f},
|
||||
IP: netip.AddrFrom4([4]byte{192, 168, 1, 2}),
|
||||
SubnetMask: netip.AddrFrom4([4]byte{255, 255, 255, 0}),
|
||||
Gateway: netip.AddrFrom4([4]byte{192, 168, 1, 1}),
|
||||
})
|
||||
netdev.UseNetdev(eth)
|
||||
|
||||
for {
|
||||
if eth.LinkStatus() != w5500.LinkStatusUp {
|
||||
println("Waiting for link to be up")
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/waveshare-epd/epd2in9v2"
|
||||
)
|
||||
|
||||
var display epd2in9v2.Device
|
||||
|
||||
func main() {
|
||||
machine.SPI0.Configure(machine.SPIConfig{
|
||||
Frequency: 12000000,
|
||||
SCK: machine.EPD_SCK_PIN,
|
||||
SDO: machine.EPD_SDO_PIN,
|
||||
})
|
||||
|
||||
display = epd2in9v2.New(
|
||||
machine.SPI0,
|
||||
machine.EPD_CS_PIN,
|
||||
machine.EPD_DC_PIN,
|
||||
machine.EPD_RESET_PIN,
|
||||
machine.EPD_BUSY_PIN,
|
||||
)
|
||||
display.Configure(epd2in9v2.Config{
|
||||
Rotation: epd2in9v2.ROTATION_270,
|
||||
Speed: epd2in9v2.SPEED_DEFAULT,
|
||||
Blocking: true,
|
||||
})
|
||||
|
||||
black := color.RGBA{0, 0, 0, 255}
|
||||
white := color.RGBA{255, 255, 255, 255}
|
||||
|
||||
// --- Step 1: clear to white ---
|
||||
println("epd2in9v2: clearing display")
|
||||
display.ClearBuffer()
|
||||
display.Display()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// --- Step 2: full refresh checkerboard ---
|
||||
println("epd2in9v2: drawing checkerboard (full refresh)")
|
||||
w, h := display.Size()
|
||||
for i := int16(0); i < w/8; i++ {
|
||||
for j := int16(0); j < h/8; j++ {
|
||||
if (i+j)%2 == 0 {
|
||||
fillRect(i*8, j*8, 8, 8, black)
|
||||
}
|
||||
}
|
||||
}
|
||||
display.Display()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// --- Step 3: fast refresh - draw border and diagonal cross ---
|
||||
println("epd2in9v2: switching to fast refresh")
|
||||
display.SetSpeed(epd2in9v2.SPEED_FAST)
|
||||
display.ClearBuffer()
|
||||
|
||||
for x := int16(0); x < w; x++ {
|
||||
display.SetPixel(x, 0, black)
|
||||
display.SetPixel(x, h-1, black)
|
||||
}
|
||||
for y := int16(0); y < h; y++ {
|
||||
display.SetPixel(0, y, black)
|
||||
display.SetPixel(w-1, y, black)
|
||||
}
|
||||
for i := int16(0); i < w && i < h; i++ {
|
||||
display.SetPixel(i, i*h/w, black)
|
||||
display.SetPixel(w-1-i, i*h/w, black)
|
||||
}
|
||||
|
||||
display.Display()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// --- Step 4: partial refresh counter ---
|
||||
println("epd2in9v2: partial refresh demo")
|
||||
display.SetSpeed(epd2in9v2.SPEED_DEFAULT)
|
||||
display.ClearBuffer()
|
||||
|
||||
println("epd2in9v2: setting base image")
|
||||
display.DisplayWithBase()
|
||||
|
||||
for count := 0; count < 10; count++ {
|
||||
cx := int16(120)
|
||||
cy := int16(50)
|
||||
fillRect(cx, cy, 60, 20, white)
|
||||
|
||||
digit := int16(count % 10)
|
||||
drawDigit(cx+22, cy+2, digit, black)
|
||||
|
||||
display.DisplayPartial()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// --- Step 5: sleep ---
|
||||
println("epd2in9v2: entering deep sleep")
|
||||
display.ClearBuffer()
|
||||
display.Display()
|
||||
display.Sleep()
|
||||
println("epd2in9v2: done, you can remove power")
|
||||
}
|
||||
|
||||
func fillRect(x, y, w, h int16, c color.RGBA) {
|
||||
for i := x; i < x+w; i++ {
|
||||
for j := y; j < y+h; j++ {
|
||||
display.SetPixel(i, j, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawDigit draws a simple 3x5-pixel-block digit (each block 4x3 px) at position (x,y).
|
||||
func drawDigit(x, y, digit int16, c color.RGBA) {
|
||||
segments := [10][5]uint8{
|
||||
{0x7, 0x5, 0x5, 0x5, 0x7}, // 0
|
||||
{0x2, 0x2, 0x2, 0x2, 0x2}, // 1
|
||||
{0x7, 0x1, 0x7, 0x4, 0x7}, // 2
|
||||
{0x7, 0x1, 0x7, 0x1, 0x7}, // 3
|
||||
{0x5, 0x5, 0x7, 0x1, 0x1}, // 4
|
||||
{0x7, 0x4, 0x7, 0x1, 0x7}, // 5
|
||||
{0x7, 0x4, 0x7, 0x5, 0x7}, // 6
|
||||
{0x7, 0x1, 0x1, 0x1, 0x1}, // 7
|
||||
{0x7, 0x5, 0x7, 0x5, 0x7}, // 8
|
||||
{0x7, 0x5, 0x7, 0x1, 0x7}, // 9
|
||||
}
|
||||
if digit < 0 || digit > 9 {
|
||||
return
|
||||
}
|
||||
for row := int16(0); row < 5; row++ {
|
||||
for col := int16(0); col < 3; col++ {
|
||||
if segments[digit][row]&(0x4>>uint(col)) != 0 {
|
||||
fillRect(x+col*4, y+row*3, 4, 3, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build arduino
|
||||
//go:build arduino || arduino_uno
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !digispark && !arduino
|
||||
//go:build !digispark && !arduino && !arduino_uno && !xiao_esp32c3
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build xiao_esp32c3
|
||||
|
||||
package main
|
||||
|
||||
import "machine"
|
||||
|
||||
func init() {
|
||||
// Replace neo in the code below to match the pin
|
||||
// that you are using if different.
|
||||
neo = machine.D6
|
||||
}
|
||||
@@ -46,6 +46,10 @@ var DefaultDeviceIdentifier = DeviceIdentifierFunc(func(id JedecID) Attrs {
|
||||
return GD25Q16C()
|
||||
case 0xC84017:
|
||||
return GD25Q64C()
|
||||
case 0x856015:
|
||||
return P25Q16H()
|
||||
case 0xEF4014:
|
||||
return W25Q80DV()
|
||||
case 0xEF4015:
|
||||
return W25Q16JVIQ()
|
||||
case 0xEF4016:
|
||||
@@ -239,6 +243,24 @@ func GD25Q64C() Attrs {
|
||||
}
|
||||
}
|
||||
|
||||
// Settings for the Puya P25Q16H 2MiB SPI flash.
|
||||
// Datasheet: https://files.seeedstudio.com/wiki/github_weiruanexample/Flash_P25Q16H-UXH-IR_Datasheet.pdf
|
||||
func P25Q16H() Attrs {
|
||||
return Attrs{
|
||||
TotalSize: 1 << 21, // 2 MiB
|
||||
StartUp: 5000 * time.Microsecond,
|
||||
JedecID: JedecID{0x85, 0x60, 0x15},
|
||||
MaxClockSpeedMHz: 55,
|
||||
QuadEnableBitMask: 0x02,
|
||||
HasSectorProtection: true,
|
||||
SupportsFastRead: true,
|
||||
SupportsQSPI: true,
|
||||
SupportsQSPIWrites: true,
|
||||
WriteStatusSplit: true,
|
||||
SingleStatusByte: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Settings for the Winbond W25Q16JV-IQ 2MiB SPI flash. Note that JV-IM has a
|
||||
// different .memory_type (0x70) Datasheet:
|
||||
// https://www.winbond.com/resource-files/w25q16jv%20spi%20revf%2005092017.pdf
|
||||
@@ -380,6 +402,25 @@ func W25Q80DL() Attrs {
|
||||
TotalSize: 1 << 20, // 1 MiB
|
||||
StartUp: 5000 * time.Microsecond,
|
||||
JedecID: JedecID{0xEF, 0x60, 0x14},
|
||||
MaxClockSpeedMHz: 80,
|
||||
QuadEnableBitMask: 0x02,
|
||||
HasSectorProtection: false,
|
||||
SupportsFastRead: true,
|
||||
SupportsQSPI: true,
|
||||
SupportsQSPIWrites: false,
|
||||
WriteStatusSplit: false,
|
||||
SingleStatusByte: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Settings for the Winbond W25Q80DV 2MiB SPI flash.
|
||||
// Datasheet:
|
||||
// https://www.winbond.com/resource-files/w25q80dv%20dl_revh_10022015.pdf
|
||||
func W25Q80DV() Attrs {
|
||||
return Attrs{
|
||||
TotalSize: 1 << 21, // 2 MiB
|
||||
StartUp: 5000 * time.Microsecond,
|
||||
JedecID: JedecID{0xEF, 0x40, 0x14},
|
||||
MaxClockSpeedMHz: 104,
|
||||
QuadEnableBitMask: 0x02,
|
||||
HasSectorProtection: false,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Guarded because still unsure of how to deal with interrupt drivers.
|
||||
//go:build tinygo
|
||||
|
||||
// Package ft6336 provides a driver for the FT6336 I2C Self-Capacitive touch
|
||||
// panel controller.
|
||||
//
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
module tinygo.org/x/drivers
|
||||
|
||||
|
||||
go 1.22.1
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
|
||||
require (
|
||||
github.com/eclipse/paho.mqtt.golang v1.2.0
|
||||
github.com/frankban/quicktest v1.10.2
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/orsinium-labs/tinymath v1.1.0
|
||||
github.com/soypat/natiu-mqtt v0.5.1
|
||||
github.com/tinygo-org/pio v0.3.0
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d
|
||||
golang.org/x/net v0.33.0
|
||||
tinygo.org/x/tinyfont v0.3.0
|
||||
|
||||
@@ -17,6 +17,8 @@ github.com/orsinium-labs/tinymath v1.1.0 h1:KomdsyLHB7vE3f1nRAJF2dyf1m/gnM2HxfTe
|
||||
github.com/orsinium-labs/tinymath v1.1.0/go.mod h1:WPXX6ei3KSXG7JfA03a+ekCYaY9SWN4I+JRl2p6ck+A=
|
||||
github.com/soypat/natiu-mqtt v0.5.1 h1:rwaDmlvjzD2+3MCOjMZc4QEkDkNwDzbct2TJbpz+TPc=
|
||||
github.com/soypat/natiu-mqtt v0.5.1/go.mod h1:xEta+cwop9izVCW7xOx2W+ct9PRMqr0gNVkvBPnQTc4=
|
||||
github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI=
|
||||
github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU=
|
||||
github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
|
||||
+18
-35
@@ -11,37 +11,18 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length")
|
||||
errInvalidNMEAChecksum = errors.New("invalid NMEA sentence checksum")
|
||||
errEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence")
|
||||
errUnknownNMEASentence = errors.New("unsupported NMEA sentence type")
|
||||
ErrInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length")
|
||||
ErrInvalidNMEASentence = errors.New("invalid NMEA sentence format")
|
||||
ErrEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence")
|
||||
ErrUnknownNMEASentence = errors.New("unsupported NMEA sentence type")
|
||||
errInvalidGSVSentence = errors.New("invalid GSV NMEA sentence")
|
||||
errInvalidGGASentence = errors.New("invalid GGA NMEA sentence")
|
||||
errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence")
|
||||
errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence")
|
||||
errGPSCommandRejected = errors.New("GPS command rejected (NAK)")
|
||||
errNoACKToGPSCommand = errors.New("no ACK to GPS command")
|
||||
)
|
||||
|
||||
type GPSError struct {
|
||||
Err error
|
||||
Info string
|
||||
Sentence string
|
||||
}
|
||||
|
||||
func newGPSError(err error, sentence string, info string) GPSError {
|
||||
return GPSError{
|
||||
Info: info,
|
||||
Err: err,
|
||||
Sentence: sentence,
|
||||
}
|
||||
}
|
||||
|
||||
func (ge GPSError) Error() string {
|
||||
return ge.Err.Error() + " " + ge.Info + " " + ge.Sentence
|
||||
}
|
||||
|
||||
func (ge GPSError) Unwrap() error {
|
||||
return ge.Err
|
||||
}
|
||||
|
||||
const (
|
||||
minimumNMEALength = 7
|
||||
startingDelimiter = '$'
|
||||
@@ -50,30 +31,34 @@ const (
|
||||
|
||||
// Device wraps a connection to a GPS device.
|
||||
type Device struct {
|
||||
buffer []byte
|
||||
bufIdx int
|
||||
sentence strings.Builder
|
||||
uart drivers.UART
|
||||
bus drivers.I2C
|
||||
address uint16
|
||||
buffer [bufferSize]byte
|
||||
}
|
||||
|
||||
// NewUART creates a new UART GPS connection. The UART must already be configured.
|
||||
func NewUART(uart drivers.UART) Device {
|
||||
return Device{
|
||||
uart: uart,
|
||||
buffer: make([]byte, bufferSize),
|
||||
bufIdx: bufferSize,
|
||||
sentence: strings.Builder{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewI2C creates a new I2C GPS connection.
|
||||
// Uses the default i2c address (0x42) for backward compatibility reasons.
|
||||
func NewI2C(bus drivers.I2C) Device {
|
||||
return NewI2CWithAddress(bus, I2C_ADDRESS)
|
||||
}
|
||||
|
||||
// NewI2CWithAddress creates a new I2C GPS connection on the provided address
|
||||
func NewI2CWithAddress(bus drivers.I2C, i2cAddress uint16) Device {
|
||||
return Device{
|
||||
bus: bus,
|
||||
address: I2C_ADDRESS,
|
||||
buffer: make([]byte, bufferSize),
|
||||
address: i2cAddress,
|
||||
bufIdx: bufferSize,
|
||||
sentence: strings.Builder{},
|
||||
}
|
||||
@@ -166,17 +151,15 @@ func (gps *Device) WriteBytes(bytes []byte) {
|
||||
// It has to end with a '*' character following by a checksum.
|
||||
func validSentence(sentence string) error {
|
||||
if len(sentence) < minimumNMEALength || sentence[0] != startingDelimiter || sentence[len(sentence)-3] != checksumDelimiter {
|
||||
return errInvalidNMEASentenceLength
|
||||
return ErrInvalidNMEASentenceLength
|
||||
}
|
||||
var cs byte = 0
|
||||
for i := 1; i < len(sentence)-3; i++ {
|
||||
cs ^= sentence[i]
|
||||
}
|
||||
checksum := strings.ToUpper(hex.EncodeToString([]byte{cs}))
|
||||
if checksum != sentence[len(sentence)-2:len(sentence)] {
|
||||
return newGPSError(errInvalidNMEAChecksum, sentence,
|
||||
"expected "+sentence[len(sentence)-2:len(sentence)]+
|
||||
" got "+checksum)
|
||||
if checksum != sentence[len(sentence)-2:] {
|
||||
return ErrInvalidNMEASentence
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+43
-4
@@ -6,12 +6,28 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type NMEASentenceType string
|
||||
|
||||
const (
|
||||
GSA NMEASentenceType = "GSA"
|
||||
GGA NMEASentenceType = "GGA"
|
||||
GLL NMEASentenceType = "GLL"
|
||||
GSV NMEASentenceType = "GSV"
|
||||
RMC NMEASentenceType = "RMC"
|
||||
VTG NMEASentenceType = "VTG"
|
||||
ZDA NMEASentenceType = "ZDA"
|
||||
TXT NMEASentenceType = "TXT"
|
||||
)
|
||||
|
||||
// Parser for GPS NMEA sentences.
|
||||
type Parser struct {
|
||||
}
|
||||
|
||||
// Fix is a GPS location fix
|
||||
type Fix struct {
|
||||
// Type is the NMEA sentence type that provided this fix.
|
||||
Type NMEASentenceType
|
||||
|
||||
// Valid if the fix was valid.
|
||||
Valid bool
|
||||
|
||||
@@ -46,13 +62,30 @@ func NewParser() Parser {
|
||||
func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
var fix Fix
|
||||
if sentence == "" {
|
||||
return fix, errEmptyNMEASentence
|
||||
return fix, ErrEmptyNMEASentence
|
||||
}
|
||||
if len(sentence) < 6 {
|
||||
return fix, errInvalidNMEASentenceLength
|
||||
return fix, ErrInvalidNMEASentenceLength
|
||||
}
|
||||
typ := sentence[3:6]
|
||||
switch typ {
|
||||
case "GSV":
|
||||
// https://docs.novatel.com/OEM7/Content/Logs/GPGSV.htm
|
||||
fields := strings.Split(sentence, ",")
|
||||
// GSV sentences have at least 4 fields, but typically 8, 12, 16, or 20 depending on satellites in view
|
||||
if len(fields) < 4 {
|
||||
return fix, errInvalidGSVSentence
|
||||
}
|
||||
|
||||
fix.Type = GSV
|
||||
|
||||
// Number of satellites in view is always field 3
|
||||
fix.Satellites = findSatellites(fields[3])
|
||||
|
||||
// GSV does not provide position, time, or fix validity
|
||||
fix.Valid = false
|
||||
|
||||
return fix, nil
|
||||
case "GGA":
|
||||
// https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm
|
||||
fields := strings.Split(sentence, ",")
|
||||
@@ -60,6 +93,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
return fix, errInvalidGGASentence
|
||||
}
|
||||
|
||||
fix.Type = GGA
|
||||
fix.Time = findTime(fields[1])
|
||||
fix.Latitude = findLatitude(fields[2], fields[3])
|
||||
fix.Longitude = findLongitude(fields[4], fields[5])
|
||||
@@ -75,6 +109,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
return fix, errInvalidGLLSentence
|
||||
}
|
||||
|
||||
fix.Type = GLL
|
||||
fix.Latitude = findLatitude(fields[1], fields[2])
|
||||
fix.Longitude = findLongitude(fields[3], fields[4])
|
||||
fix.Time = findTime(fields[5])
|
||||
@@ -89,6 +124,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
return fix, errInvalidRMCSentence
|
||||
}
|
||||
|
||||
fix.Type = RMC
|
||||
fix.Time = findTime(fields[1])
|
||||
fix.Valid = (fields[2] == "A")
|
||||
fix.Latitude = findLatitude(fields[3], fields[4])
|
||||
@@ -96,12 +132,15 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
fix.Speed = findSpeed(fields[7])
|
||||
fix.Heading = findHeading(fields[8])
|
||||
date := findDate(fields[9])
|
||||
fix.Time = fix.Time.AddDate(date.Year(), int(date.Month()), date.Day())
|
||||
fix.Time = date.Add(time.Duration(fix.Time.Hour())*time.Hour +
|
||||
time.Duration(fix.Time.Minute())*time.Minute +
|
||||
time.Duration(fix.Time.Second())*time.Second +
|
||||
time.Duration(fix.Time.Nanosecond())*time.Nanosecond)
|
||||
|
||||
return fix, nil
|
||||
}
|
||||
|
||||
return fix, newGPSError(errUnknownNMEASentence, sentence, typ)
|
||||
return fix, ErrUnknownNMEASentence
|
||||
}
|
||||
|
||||
// findTime returns the time from an NMEA sentence:
|
||||
|
||||
+21
-5
@@ -8,13 +8,29 @@ import (
|
||||
)
|
||||
|
||||
func TestParseUnknownSentence(t *testing.T) {
|
||||
p := NewParser()
|
||||
|
||||
val := "$GPVTG,89.68,T,,M,0.00,N,0.0,K*5F"
|
||||
_, err := p.Parse(val)
|
||||
if err == nil {
|
||||
t.Error("should have unknown sentence err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGSV(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
p := NewParser()
|
||||
|
||||
val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F"
|
||||
_, err := p.Parse(val)
|
||||
c.Assert(err.Error(), qt.Contains, "unsupported NMEA sentence type")
|
||||
fix, err := p.Parse(val)
|
||||
if err != nil {
|
||||
t.Error("should have parsed")
|
||||
}
|
||||
|
||||
c.Assert(fix.Type, qt.Equals, GSV)
|
||||
c.Assert(fix.Satellites, qt.Equals, int16(9))
|
||||
c.Assert(fix.Valid, qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestParseGGA(t *testing.T) {
|
||||
@@ -70,15 +86,15 @@ func TestParseRMC(t *testing.T) {
|
||||
t.Error("should have errInvalidRMCSentence error")
|
||||
}
|
||||
|
||||
val = "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,130522,0.0,E,D*2B"
|
||||
val = "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,010622,0.0,E,D*2B"
|
||||
fix, err := p.Parse(val)
|
||||
if err != nil {
|
||||
t.Error("should have parsed")
|
||||
}
|
||||
|
||||
c.Assert(fix.Time.Year(), qt.Equals, 2022)
|
||||
c.Assert(fix.Time.Month(), qt.Equals, time.May)
|
||||
c.Assert(fix.Time.Day(), qt.Equals, 13)
|
||||
c.Assert(fix.Time.Month(), qt.Equals, time.June)
|
||||
c.Assert(fix.Time.Day(), qt.Equals, 1)
|
||||
c.Assert(fix.Time.Hour(), qt.Equals, 20)
|
||||
c.Assert(fix.Time.Minute(), qt.Equals, 35)
|
||||
c.Assert(fix.Time.Second(), qt.Equals, 22)
|
||||
|
||||
+5
-1
@@ -4,7 +4,11 @@ package gps
|
||||
|
||||
// The I2C address which this device listens to.
|
||||
const (
|
||||
I2C_ADDRESS = 0x42
|
||||
// To ensure backward compatibility
|
||||
I2C_ADDRESS = UBLOX_I2C_ADDRESS
|
||||
|
||||
UBLOX_I2C_ADDRESS = 0x42
|
||||
PA1010D_I2C_ADDRESS = 0x10
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
+240
-39
@@ -1,53 +1,254 @@
|
||||
package gps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// flight mode disables the GPS COCOM limits
|
||||
var flight_mode_cmd = [...]byte{
|
||||
0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0xFF, 0xFF, 0x06, 0x03, 0x00, 0x00, 0x00,
|
||||
0x00, 0x10, 0x27, 0x00, 0x00, 0x05, 0x00, 0xFA, 0x00, 0xFA, 0x00, 0x64, 0x00,
|
||||
0x2C, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x16, 0xDC}
|
||||
|
||||
// Sets CFG-GNSS to disable everything other than GPS GNSS
|
||||
// solution. Failure to do this means GPS power saving
|
||||
// doesn't work. Not needed for MAX7, needed for MAX8's
|
||||
var cfg_gnss_cmd = [...]byte{
|
||||
0xB5, 0x62, 0x06, 0x3E, 0x2C, 0x00, 0x00, 0x00,
|
||||
0x20, 0x05, 0x00, 0x08, 0x10, 0x00, 0x01, 0x00,
|
||||
0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0x03, 0x08, 0x10, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0x05, 0x00, 0x03, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0xFC, 0x11}
|
||||
|
||||
func FlightMode(d Device) (err error) {
|
||||
err = sendCommand(d, flight_mode_cmd[:])
|
||||
return err
|
||||
// FlightModeCmd is a UBX-CFG-NAV5 command
|
||||
var nav5Cmd = CfgNav5{
|
||||
Mask: CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode,
|
||||
DynModel: DynModeAirborne1g, // Airborne with <1g acceleration
|
||||
FixMode: FixModeAuto, // Auto 2D/3D
|
||||
MinElev_deg: 5, // Minimum elevation 5 degrees
|
||||
FixedAlt_me2: 0, // Not used
|
||||
FixedAltVar_m2e4: 0, // Not used
|
||||
PDop: 100, // 10.0
|
||||
TDop: 100, // 10.0
|
||||
PAcc_m: 5000, // 5 meters
|
||||
TAcc_m: 5000, // 5 meters
|
||||
StaticHoldThresh_cm_s: 0, // Not used
|
||||
DgnssTimeout_s: 0, // Not used
|
||||
CnoThreshNumSVs: 0, // Not used
|
||||
CnoThresh_dbhz: 0, // Not used
|
||||
StaticHoldMaxDist_m: 0, // Not used
|
||||
UtcStandard: 0, // Automatic
|
||||
Reserved1: [2]byte{},
|
||||
Reserved2: [5]byte{},
|
||||
}
|
||||
|
||||
func SetCfgGNSS(d Device) (err error) {
|
||||
err = sendCommand(d, cfg_gnss_cmd[:])
|
||||
return err
|
||||
// SetFlightMode sends UBX-CFG-NAV5 command to set GPS into flight mode
|
||||
func (d *Device) SetFlightMode() (err error) {
|
||||
nav5Cmd.DynModel = DynModeAirborne1g
|
||||
nav5Cmd.FixMode = FixModeAuto
|
||||
nav5Cmd.Put42Bytes(d.buffer[:])
|
||||
|
||||
return d.SendCommand(d.buffer[:42])
|
||||
}
|
||||
|
||||
func sendCommand(d Device, command []byte) (err error) {
|
||||
d.WriteBytes(command)
|
||||
// SetPedestrianMode sends UBX-CFG-NAV5 command to set GPS into pedestrian mode
|
||||
func (d *Device) SetPedestrianMode() (err error) {
|
||||
nav5Cmd.DynModel = DynModePedestrian
|
||||
nav5Cmd.FixMode = FixModeAuto
|
||||
nav5Cmd.Put42Bytes(d.buffer[:])
|
||||
|
||||
return d.SendCommand(d.buffer[:42])
|
||||
}
|
||||
|
||||
// SetAutomotiveMode sends UBX-CFG-NAV5 command to set GPS into automotive mode
|
||||
func (d *Device) SetAutomotiveMode() (err error) {
|
||||
nav5Cmd.DynModel = DynModeAutomotive
|
||||
nav5Cmd.FixMode = FixModeAuto
|
||||
nav5Cmd.Put42Bytes(d.buffer[:])
|
||||
|
||||
return d.SendCommand(d.buffer[:42])
|
||||
}
|
||||
|
||||
// SetBikeMode sends UBX-CFG-NAV5 command to set GPS into bike mode
|
||||
func (d *Device) SetBikeMode() (err error) {
|
||||
nav5Cmd.DynModel = DynModeBike
|
||||
nav5Cmd.FixMode = FixModeAuto
|
||||
nav5Cmd.Put42Bytes(d.buffer[:])
|
||||
|
||||
return d.SendCommand(d.buffer[:42])
|
||||
}
|
||||
|
||||
var (
|
||||
// GGA (time, lat/lng, altitude)
|
||||
messageRateGGACmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x00,
|
||||
Rate: 1, // Every position fix
|
||||
}
|
||||
// GLL (time, lat/lng)
|
||||
messageRateGLLCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x01,
|
||||
Rate: 1, // Every position fix
|
||||
}
|
||||
// GSA (satellite id list)
|
||||
messageRateGSACmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x02,
|
||||
Rate: 0, // Disabled
|
||||
}
|
||||
// GSV (satellite locations)
|
||||
messageRateGSVCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x03,
|
||||
Rate: 0, // Every position fix
|
||||
}
|
||||
// RMC (time, lat/lng, speed, course)
|
||||
messageRateRMCCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x04,
|
||||
Rate: 1, // Every position fix
|
||||
}
|
||||
// VTG (speed, course)
|
||||
messageRateVTGCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x05,
|
||||
Rate: 0, // Disabled
|
||||
}
|
||||
// ZDA (time, timezone)
|
||||
messageRateZDACmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x08,
|
||||
Rate: 0, // Disabled
|
||||
}
|
||||
// TXT (text transmission)
|
||||
messageRateTXTCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x41,
|
||||
Rate: 0, // Disabled
|
||||
}
|
||||
)
|
||||
|
||||
// SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences:
|
||||
// GSV, GGA, GLL, and RMC only.
|
||||
func SetMessageRatesMinimal(d *Device) (err error) {
|
||||
commands := []CfgMsg1{
|
||||
messageRateGSACmd,
|
||||
messageRateGLLCmd,
|
||||
messageRateVTGCmd,
|
||||
messageRateZDACmd,
|
||||
messageRateTXTCmd,
|
||||
}
|
||||
for i := range commands {
|
||||
commands[i].Rate = 0 // Disable
|
||||
}
|
||||
return setCfg1s(d, commands)
|
||||
}
|
||||
|
||||
// SetMessageRatesAllEnabled configures the GPS to output all NMEA sentences
|
||||
func SetMessageRatesAllEnabled(d *Device) (err error) {
|
||||
commands := []CfgMsg1{
|
||||
messageRateGSACmd,
|
||||
messageRateGGACmd,
|
||||
messageRateGLLCmd,
|
||||
messageRateGSVCmd,
|
||||
messageRateRMCCmd,
|
||||
messageRateVTGCmd,
|
||||
messageRateZDACmd,
|
||||
messageRateTXTCmd,
|
||||
}
|
||||
for i := range commands {
|
||||
commands[i].Rate = 1 // Enable
|
||||
}
|
||||
return setCfg1s(d, commands)
|
||||
}
|
||||
|
||||
func setCfg1s(d *Device, commands []CfgMsg1) (err error) {
|
||||
var buf [9]byte
|
||||
for _, cmd := range commands {
|
||||
cmd.Put9Bytes(buf[:])
|
||||
// TODO handle errors differently here?
|
||||
// This implementation just saves the last error and continues.
|
||||
// Due to the GPS modules sending updates asynchronously
|
||||
// the response is interleaved along with regular ASCII
|
||||
// NMEA messages.
|
||||
err = d.SendCommand(buf[:])
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// gnssDisableCmd is a UBX-CFG-GNSS command to disable all GNSS but GPS
|
||||
// Needed for MAX8's, not needed for MAX7
|
||||
var gnssDisableCmd = CfgGnss{
|
||||
MsgVer: 0x00,
|
||||
NumTrkChHw: 0x20, // 32 channels
|
||||
NumTrkChUse: 0x20,
|
||||
ConfigBlocks: []CfgGnssConfigBlocksType{
|
||||
{GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, // GPS enabled
|
||||
{GnssId: 1, ResTrkCh: 1, MaxTrkCh: 3, Flags: 0x010000}, // SBAS disabled
|
||||
{GnssId: 3, ResTrkCh: 8, MaxTrkCh: 16, Flags: 0x010000}, // BeiDou disabled
|
||||
{GnssId: 5, ResTrkCh: 0, MaxTrkCh: 3, Flags: 0x010000}, // QZSS disabled
|
||||
{GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: 0x010000}, // GLONASS disabled
|
||||
},
|
||||
}
|
||||
|
||||
// SetGNSSDisable sends UBX-CFG-GNSS command to disable all GNSS but GPS
|
||||
func (d *Device) SetGNSSDisable() (err error) {
|
||||
err = gnssDisableCmd.Put(d.buffer[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.SendCommand(d.buffer[:])
|
||||
}
|
||||
|
||||
// SendCommand sends a UBX command and waits for ACK/NAK response
|
||||
func (d *Device) SendCommand(command []byte) error {
|
||||
// Calculate and append checksum
|
||||
checksummed := appendChecksum(command)
|
||||
d.WriteBytes(checksummed)
|
||||
|
||||
start := time.Now()
|
||||
for time.Now().Sub(start) < 1000 {
|
||||
if d.readNextByte() == '\n' {
|
||||
if d.readNextByte() == 0xB5 {
|
||||
d.readNextByte()
|
||||
if d.readNextByte() == 0x05 {
|
||||
if d.readNextByte() == 0x01 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for time.Since(start) < time.Second {
|
||||
// Look for UBX sync sequence
|
||||
if d.readNextByte() != ubxSyncChar1 {
|
||||
continue
|
||||
}
|
||||
if d.readNextByte() != ubxSyncChar2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read message class and ID
|
||||
msgClass := d.readNextByte()
|
||||
msgID := d.readNextByte()
|
||||
|
||||
// Check if it's an ACK class message
|
||||
if msgClass != ubxClassACK {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read length (2 bytes, little-endian) - ACK is always 2 bytes payload
|
||||
lenLo := d.readNextByte()
|
||||
lenHi := d.readNextByte()
|
||||
length := uint16(lenLo) | uint16(lenHi)<<8
|
||||
|
||||
if length != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read ACK payload: class and ID of acknowledged message
|
||||
ackClass := d.readNextByte()
|
||||
ackID := d.readNextByte()
|
||||
|
||||
// Verify ACK is for our command (command[2] = class, command[3] = ID)
|
||||
if ackClass != command[2] || ackID != command[3] {
|
||||
continue
|
||||
}
|
||||
|
||||
if msgID == ubxACK_ACK {
|
||||
return nil
|
||||
}
|
||||
if msgID == ubxACK_NAK {
|
||||
return errGPSCommandRejected
|
||||
}
|
||||
}
|
||||
return errors.New("no ACK to GPS command")
|
||||
|
||||
return errNoACKToGPSCommand
|
||||
}
|
||||
|
||||
// appendChecksum calculates UBX checksum and appends it to the message
|
||||
func appendChecksum(msg []byte) []byte {
|
||||
var ckA, ckB byte
|
||||
// Checksum covers class, ID, length, and payload (skip sync chars)
|
||||
for i := 2; i < len(msg); i++ {
|
||||
ckA += msg[i]
|
||||
ckB += ckA
|
||||
}
|
||||
return append(msg, ckA, ckB)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
package gps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendChecksum(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected []byte
|
||||
}{
|
||||
{
|
||||
name: "simple message",
|
||||
input: []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00},
|
||||
expected: []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00, 0x2A, 0x84},
|
||||
},
|
||||
{
|
||||
name: "CFG-NAV5 header only",
|
||||
input: []byte{0xB5, 0x62, 0x06, 0x24, 0x24, 0x00},
|
||||
expected: []byte{0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0x4E, 0xCC},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := appendChecksum(tc.input)
|
||||
|
||||
if len(result) != len(tc.expected) {
|
||||
t.Errorf("expected length %d, got %d", len(tc.expected), len(result))
|
||||
return
|
||||
}
|
||||
|
||||
// Check checksum bytes (last two bytes)
|
||||
ckA := result[len(result)-2]
|
||||
ckB := result[len(result)-1]
|
||||
expectedCkA := tc.expected[len(tc.expected)-2]
|
||||
expectedCkB := tc.expected[len(tc.expected)-1]
|
||||
|
||||
if ckA != expectedCkA || ckB != expectedCkB {
|
||||
t.Errorf("expected checksum 0x%02X 0x%02X, got 0x%02X 0x%02X",
|
||||
expectedCkA, expectedCkB, ckA, ckB)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendChecksumPreservesOriginal(t *testing.T) {
|
||||
input := []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00}
|
||||
original := make([]byte, len(input))
|
||||
copy(original, input)
|
||||
|
||||
result := appendChecksum(input)
|
||||
|
||||
// Verify original bytes are preserved
|
||||
for i := range input {
|
||||
if result[i] != original[i] {
|
||||
t.Errorf("byte %d changed: expected 0x%02X, got 0x%02X", i, original[i], result[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Verify two bytes were appended
|
||||
if len(result) != len(input)+2 {
|
||||
t.Errorf("expected length %d, got %d", len(input)+2, len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNav5CmdConfig(t *testing.T) {
|
||||
// Verify nav5Cmd has expected values
|
||||
if nav5Cmd.DynModel != 6 {
|
||||
t.Errorf("expected DynModel 6 (airborne <1g), got %d", nav5Cmd.DynModel)
|
||||
}
|
||||
|
||||
if nav5Cmd.FixMode != 3 {
|
||||
t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", nav5Cmd.FixMode)
|
||||
}
|
||||
|
||||
expectedMask := CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode
|
||||
if nav5Cmd.Mask != expectedMask {
|
||||
t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, nav5Cmd.Mask)
|
||||
}
|
||||
|
||||
if nav5Cmd.MinElev_deg != 5 {
|
||||
t.Errorf("expected MinElev_deg 5, got %d", nav5Cmd.MinElev_deg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGNSSDisableCmdConfig(t *testing.T) {
|
||||
// Verify GNSSDisableCmd has expected structure
|
||||
if gnssDisableCmd.MsgVer != 0 {
|
||||
t.Errorf("expected MsgVer 0, got %d", gnssDisableCmd.MsgVer)
|
||||
}
|
||||
|
||||
if gnssDisableCmd.NumTrkChHw != 0x20 {
|
||||
t.Errorf("expected NumTrkChHw 0x20, got 0x%02X", gnssDisableCmd.NumTrkChHw)
|
||||
}
|
||||
|
||||
if len(gnssDisableCmd.ConfigBlocks) != 5 {
|
||||
t.Errorf("expected 5 config blocks, got %d", len(gnssDisableCmd.ConfigBlocks))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify GPS is enabled
|
||||
gpsBlock := gnssDisableCmd.ConfigBlocks[0]
|
||||
if gpsBlock.GnssId != 0 {
|
||||
t.Errorf("expected first block GnssId 0 (GPS), got %d", gpsBlock.GnssId)
|
||||
}
|
||||
if gpsBlock.Flags&CfgGnssEnable == 0 {
|
||||
t.Error("expected GPS to be enabled")
|
||||
}
|
||||
|
||||
// Verify other GNSS are disabled
|
||||
for i := 1; i < len(gnssDisableCmd.ConfigBlocks); i++ {
|
||||
block := gnssDisableCmd.ConfigBlocks[i]
|
||||
if block.Flags&CfgGnssEnable != 0 {
|
||||
t.Errorf("expected block %d (GnssId %d) to be disabled", i, block.GnssId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNav5CmdWrite(t *testing.T) {
|
||||
buf := make([]byte, 64)
|
||||
nav5Cmd.Put42Bytes(buf)
|
||||
|
||||
// Verify sync chars
|
||||
if buf[0] != 0xB5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Verify class/id
|
||||
if buf[2] != 0x06 || buf[3] != 0x24 {
|
||||
t.Errorf("expected class/id 0x06 0x24, got 0x%02X 0x%02X", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Verify DynModel at offset 8
|
||||
if buf[8] != 6 {
|
||||
t.Errorf("expected DynModel 6, got %d", buf[8])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGNSSDisableCmdWrite(t *testing.T) {
|
||||
buf := make([]byte, 64)
|
||||
err := gnssDisableCmd.Put(buf)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error, likely buffer too short for data: %v", err)
|
||||
}
|
||||
|
||||
// 6 header + 4 payload header + 5*8 blocks = 50 bytes
|
||||
const expectedLen = 6 + 4 + 5*8
|
||||
sz := gnssDisableCmd.Size()
|
||||
if sz != expectedLen {
|
||||
t.Errorf("expected %d bytes, got %d", expectedLen, sz)
|
||||
}
|
||||
|
||||
// Verify sync chars
|
||||
if buf[0] != 0xB5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Verify class/id
|
||||
if buf[2] != 0x06 || buf[3] != 0x3E {
|
||||
t.Errorf("expected class/id 0x06 0x3E, got 0x%02X 0x%02X", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Verify number of blocks
|
||||
if buf[9] != 5 {
|
||||
t.Errorf("expected 5 blocks, got %d", buf[9])
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecksumCalculation(t *testing.T) {
|
||||
// Test with known UBX message and expected checksum
|
||||
// This is a minimal CFG-NAV5 poll message
|
||||
msg := []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00}
|
||||
|
||||
result := appendChecksum(msg)
|
||||
|
||||
// Verify checksum by recalculating
|
||||
var ckA, ckB byte
|
||||
for i := 2; i < len(msg); i++ {
|
||||
ckA += msg[i]
|
||||
ckB += ckA
|
||||
}
|
||||
|
||||
if result[6] != ckA || result[7] != ckB {
|
||||
t.Errorf("checksum mismatch: expected 0x%02X 0x%02X, got 0x%02X 0x%02X",
|
||||
ckA, ckB, result[6], result[7])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageRateCmdConfigs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cmd CfgMsg1
|
||||
msgClass byte
|
||||
msgID byte
|
||||
rate byte
|
||||
}{
|
||||
{"GGA", messageRateGGACmd, 0xF0, 0x00, 1},
|
||||
{"GLL", messageRateGLLCmd, 0xF0, 0x01, 1},
|
||||
{"GSA", messageRateGSACmd, 0xF0, 0x02, 0},
|
||||
{"GSV", messageRateGSVCmd, 0xF0, 0x03, 0},
|
||||
{"RMC", messageRateRMCCmd, 0xF0, 0x04, 1},
|
||||
{"VTG", messageRateVTGCmd, 0xF0, 0x05, 0},
|
||||
{"ZDA", messageRateZDACmd, 0xF0, 0x08, 0},
|
||||
{"TXT", messageRateTXTCmd, 0xF0, 0x41, 0},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.cmd.MsgClass != tc.msgClass {
|
||||
t.Errorf("expected MsgClass 0x%02X, got 0x%02X", tc.msgClass, tc.cmd.MsgClass)
|
||||
}
|
||||
if tc.cmd.MsgID != tc.msgID {
|
||||
t.Errorf("expected MsgID 0x%02X, got 0x%02X", tc.msgID, tc.cmd.MsgID)
|
||||
}
|
||||
if tc.cmd.Rate != tc.rate {
|
||||
t.Errorf("expected Rate %d, got %d", tc.rate, tc.cmd.Rate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgMsg1Write(t *testing.T) {
|
||||
cmd := CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x00,
|
||||
Rate: 1,
|
||||
}
|
||||
|
||||
buf := make([]byte, 16)
|
||||
cmd.Put9Bytes(buf)
|
||||
// Verify sync chars
|
||||
if buf[0] != 0xB5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Verify class/id (0x06 0x01 for CFG-MSG)
|
||||
if buf[2] != 0x06 || buf[3] != 0x01 {
|
||||
t.Errorf("expected class/id 0x06 0x01, got 0x%02X 0x%02X", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Verify length (3 bytes payload)
|
||||
if buf[4] != 3 || buf[5] != 0 {
|
||||
t.Errorf("expected length 3, got %d", uint16(buf[4])|uint16(buf[5])<<8)
|
||||
}
|
||||
|
||||
// Verify payload
|
||||
if buf[6] != 0xF0 {
|
||||
t.Errorf("expected MsgClass 0xF0, got 0x%02X", buf[6])
|
||||
}
|
||||
if buf[7] != 0x00 {
|
||||
t.Errorf("expected MsgID 0x00, got 0x%02X", buf[7])
|
||||
}
|
||||
if buf[8] != 1 {
|
||||
t.Errorf("expected Rate 1, got %d", buf[8])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgMsg1ClassID(t *testing.T) {
|
||||
cmd := CfgMsg1{}
|
||||
if got := cmd.classID(); got != 0x0106 {
|
||||
t.Errorf("expected 0x0106, got 0x%04x", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinimalMessageRatesConfig(t *testing.T) {
|
||||
// Verify the minimal config has correct rates set
|
||||
// GGA and RMC should be enabled (rate=1), others disabled (rate=0)
|
||||
expectedRates := map[byte]byte{
|
||||
0x00: 1, // GGA - enabled
|
||||
0x01: 1, // GLL - enabled
|
||||
0x02: 0, // GSA - disabled
|
||||
0x03: 0, // GSV - disabled
|
||||
0x04: 1, // RMC - enabled
|
||||
0x05: 0, // VTG - disabled
|
||||
0x08: 0, // ZDA - disabled
|
||||
0x41: 0, // TXT - disabled
|
||||
}
|
||||
|
||||
commands := []CfgMsg1{
|
||||
messageRateGGACmd,
|
||||
messageRateGLLCmd,
|
||||
messageRateGSACmd,
|
||||
messageRateGSVCmd,
|
||||
messageRateRMCCmd,
|
||||
messageRateVTGCmd,
|
||||
messageRateZDACmd,
|
||||
messageRateTXTCmd,
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
expectedRate, ok := expectedRates[cmd.MsgID]
|
||||
if !ok {
|
||||
t.Errorf("unexpected MsgID 0x%02X", cmd.MsgID)
|
||||
continue
|
||||
}
|
||||
if cmd.Rate != expectedRate {
|
||||
t.Errorf("MsgID 0x%02X: expected rate %d, got %d", cmd.MsgID, expectedRate, cmd.Rate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllMessageRatesWriteCorrectBytes(t *testing.T) {
|
||||
// Test that each message rate command writes the correct bytes
|
||||
commands := []CfgMsg1{
|
||||
messageRateGGACmd,
|
||||
messageRateGLLCmd,
|
||||
messageRateGSACmd,
|
||||
messageRateGSVCmd,
|
||||
messageRateRMCCmd,
|
||||
messageRateVTGCmd,
|
||||
messageRateZDACmd,
|
||||
messageRateTXTCmd,
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
buf := make([]byte, 16)
|
||||
cmd.Put9Bytes(buf)
|
||||
|
||||
// Verify MsgClass in payload
|
||||
if buf[6] != 0xF0 {
|
||||
t.Errorf("MsgID 0x%02X: expected MsgClass 0xF0, got 0x%02X", cmd.MsgID, buf[6])
|
||||
}
|
||||
|
||||
// Verify MsgID in payload
|
||||
if buf[7] != cmd.MsgID {
|
||||
t.Errorf("expected MsgID 0x%02X in payload, got 0x%02X", cmd.MsgID, buf[7])
|
||||
}
|
||||
|
||||
// Verify Rate in payload
|
||||
if buf[8] != cmd.Rate {
|
||||
t.Errorf("MsgID 0x%02X: expected Rate %d, got %d", cmd.MsgID, cmd.Rate, buf[8])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMessageRatesAllEnabledModifiesRate(t *testing.T) {
|
||||
// Verify that when we copy a command and set Rate=1, it works correctly
|
||||
cmd := messageRateGSACmd // This one is disabled by default
|
||||
if cmd.Rate != 0 {
|
||||
t.Errorf("expected GSA default rate 0, got %d", cmd.Rate)
|
||||
}
|
||||
|
||||
// Simulate what SetMessageRatesAllEnabled does
|
||||
cmd.Rate = 1
|
||||
|
||||
buf := make([]byte, 16)
|
||||
cmd.Put9Bytes(buf)
|
||||
if buf[8] != 1 {
|
||||
t.Errorf("expected Rate 1 in buffer, got %d", buf[8])
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
package gps
|
||||
|
||||
import "io"
|
||||
|
||||
// UBX message classes
|
||||
const (
|
||||
ubxClassACK = 0x05
|
||||
)
|
||||
|
||||
// UBX ACK message IDs
|
||||
const (
|
||||
ubxACK_NAK = 0x00 // Message not acknowledged
|
||||
ubxACK_ACK = 0x01 // Message acknowledged
|
||||
)
|
||||
|
||||
// UBX sync characters
|
||||
const (
|
||||
ubxSyncChar1 = 0xB5
|
||||
ubxSyncChar2 = 0x62
|
||||
)
|
||||
|
||||
const (
|
||||
DynModePortable = 0
|
||||
DynModeStationary = 2
|
||||
DynModePedestrian = 3
|
||||
DynModeAutomotive = 4
|
||||
DynModeSea = 5
|
||||
DynModeAirborne1g = 6
|
||||
DynModeAirborne2g = 7
|
||||
DynModeAirborne4g = 8
|
||||
DynModeWristWatch = 9
|
||||
DynModeBike = 10
|
||||
)
|
||||
|
||||
const (
|
||||
FixMode2D = 1
|
||||
FixMode3D = 2
|
||||
FixModeAuto = 3
|
||||
)
|
||||
|
||||
// from https://github.com/daedaleanai/ublox/blob/main/ubx/messages.go
|
||||
|
||||
// Message ubx-cfg-nav5
|
||||
|
||||
// CfgNav5 (Get/set) Navigation engine settings
|
||||
// Class/Id 0x06 0x24 (36 bytes)
|
||||
// See the Navigation Configuration Settings Description for a detailed description of how these settings affect receiver operation.
|
||||
type CfgNav5 struct {
|
||||
Mask CfgNav5Mask // Parameters bitmask. Only the masked parameters will be applied.
|
||||
DynModel byte // Dynamic platform model: 0: portable 2: stationary 3: pedestrian 4: automotive 5: sea 6: airborne with <1g acceleration 7: airborne with <2g acceleration 8: airborne with <4g acceleration 9: wrist-worn watch (not supported in protocol versions less than 18) 10: bike (supported in protocol versions 19. 2)
|
||||
FixMode byte // Position fixing mode: 1: 2D only 2: 3D only 3: auto 2D/3D
|
||||
FixedAlt_me2 int32 // [1e-2 m] Fixed altitude (mean sea level) for 2D fix mode
|
||||
FixedAltVar_m2e4 uint32 // [1e-4 m^2] Fixed altitude variance for 2D mode
|
||||
MinElev_deg int8 // [deg] Minimum elevation for a GNSS satellite to be used in NAV
|
||||
DrLimit_s byte // [s] Reserved
|
||||
PDop uint16 // Position DOP mask to use
|
||||
TDop uint16 // Time DOP mask to use
|
||||
PAcc_m uint16 // [m] Position accuracy mask
|
||||
TAcc_m uint16 // [m] Time accuracy mask
|
||||
StaticHoldThresh_cm_s byte // [cm/s] Static hold threshold
|
||||
DgnssTimeout_s byte // [s] DGNSS timeout
|
||||
CnoThreshNumSVs byte // Number of satellites required to have C/N0 above cnoThresh for a fix to be attempted
|
||||
CnoThresh_dbhz byte // [dBHz] C/N0 threshold for deciding whether to attempt a fix
|
||||
Reserved1 [2]byte // Reserved
|
||||
StaticHoldMaxDist_m uint16 // [m] Static hold distance threshold (before quitting static hold)
|
||||
UtcStandard byte // UTC standard to be used: 0: Automatic; receiver selects based on GNSS configuration (see GNSS time bases) 3: UTC as operated by the U.S. Naval Observatory (USNO); derived from GPS time 5: UTC as combined from multiple European laboratories; derived from Galileo time 6: UTC as operated by the former Soviet Union (SU); derived from GLONASS time 7: UTC as operated by the National Time Service Center (NTSC), China; derived from BeiDou time (not supported in protocol versions less than 16).
|
||||
Reserved2 [5]byte // Reserved
|
||||
}
|
||||
|
||||
func (CfgNav5) classID() uint16 { return 0x2406 }
|
||||
|
||||
type CfgNav5Mask uint16
|
||||
|
||||
var _ io.WriterTo = CfgNav5{} // compile time guarantee of interface implementation.
|
||||
|
||||
const (
|
||||
CfgNav5Dyn CfgNav5Mask = 0x1 // Apply dynamic model settings
|
||||
CfgNav5MinEl CfgNav5Mask = 0x2 // Apply minimum elevation settings
|
||||
CfgNav5PosFixMode CfgNav5Mask = 0x4 // Apply fix mode settings
|
||||
CfgNav5DrLim CfgNav5Mask = 0x8 // Reserved
|
||||
CfgNav5PosMask CfgNav5Mask = 0x10 // Apply position mask settings
|
||||
CfgNav5TimeMask CfgNav5Mask = 0x20 // Apply time mask settings
|
||||
CfgNav5StaticHoldMask CfgNav5Mask = 0x40 // Apply static hold settings
|
||||
CfgNav5DgpsMask CfgNav5Mask = 0x80 // Apply DGPS settings
|
||||
CfgNav5CnoThreshold CfgNav5Mask = 0x100 // Apply CNO threshold settings (cnoThresh, cnoThreshNumSVs)
|
||||
CfgNav5Utc CfgNav5Mask = 0x400 // Apply UTC settings (not supported in protocol versions less than 16).
|
||||
)
|
||||
|
||||
func (cfg CfgNav5) Append(dst []byte) []byte {
|
||||
var buf [42]byte
|
||||
cfg.Put42Bytes(buf[:])
|
||||
dst = append(dst, buf[:]...)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (cfg CfgNav5) WriteTo(w io.Writer) (int64, error) {
|
||||
var buf [42]byte
|
||||
cfg.Put42Bytes(buf[:])
|
||||
n, err := w.Write(buf[:])
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// Write CfgNav5 message to buffer
|
||||
func (cfg CfgNav5) Put42Bytes(buf []byte) {
|
||||
_ = buf[41]
|
||||
copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 36, 0})
|
||||
|
||||
buf[6] = byte(cfg.Mask)
|
||||
buf[7] = byte(cfg.Mask >> 8)
|
||||
buf[8] = cfg.DynModel
|
||||
buf[9] = cfg.FixMode
|
||||
buf[10] = byte(cfg.FixedAlt_me2)
|
||||
buf[11] = byte(cfg.FixedAlt_me2 >> 8)
|
||||
buf[12] = byte(cfg.FixedAlt_me2 >> 16)
|
||||
buf[13] = byte(cfg.FixedAlt_me2 >> 24)
|
||||
buf[14] = byte(cfg.FixedAltVar_m2e4)
|
||||
buf[15] = byte(cfg.FixedAltVar_m2e4 >> 8)
|
||||
buf[16] = byte(cfg.FixedAltVar_m2e4 >> 16)
|
||||
buf[17] = byte(cfg.FixedAltVar_m2e4 >> 24)
|
||||
buf[18] = byte(cfg.MinElev_deg)
|
||||
buf[19] = cfg.DrLimit_s
|
||||
buf[20] = byte(cfg.PDop)
|
||||
buf[21] = byte(cfg.PDop >> 8)
|
||||
buf[22] = byte(cfg.TDop)
|
||||
buf[23] = byte(cfg.TDop >> 8)
|
||||
buf[24] = byte(cfg.PAcc_m)
|
||||
buf[25] = byte(cfg.PAcc_m >> 8)
|
||||
buf[26] = byte(cfg.TAcc_m)
|
||||
buf[27] = byte(cfg.TAcc_m >> 8)
|
||||
buf[28] = cfg.StaticHoldThresh_cm_s
|
||||
buf[29] = cfg.DgnssTimeout_s
|
||||
buf[30] = cfg.CnoThreshNumSVs
|
||||
buf[31] = cfg.CnoThresh_dbhz
|
||||
copy(buf[32:34], cfg.Reserved1[:])
|
||||
buf[34] = byte(cfg.StaticHoldMaxDist_m)
|
||||
buf[35] = byte(cfg.StaticHoldMaxDist_m >> 8)
|
||||
buf[36] = cfg.UtcStandard
|
||||
copy(buf[37:42], cfg.Reserved2[:])
|
||||
}
|
||||
|
||||
// Message ubx-cfg-msg
|
||||
|
||||
// CfgMsg1 (Get/set) Set message rate
|
||||
// Class/Id 0x06 0x01 (3 bytes)
|
||||
// Set message rate configuration for the current port. See also section How to change between protocols.
|
||||
type CfgMsg1 struct {
|
||||
MsgClass byte // Message class
|
||||
MsgID byte // Message identifier
|
||||
Rate byte // Send rate on current port
|
||||
}
|
||||
|
||||
func (CfgMsg1) classID() uint16 { return 0x0106 }
|
||||
|
||||
func (cfg CfgMsg1) Put9Bytes(buf []byte) {
|
||||
copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 3, 0})
|
||||
buf[6] = cfg.MsgClass
|
||||
buf[7] = cfg.MsgID
|
||||
buf[8] = cfg.Rate
|
||||
}
|
||||
|
||||
// Message ubx-cfg-gnss
|
||||
|
||||
// CfgGnss (Get/set) GNSS system configuration
|
||||
// Class/Id 0x06 0x3e (4 + N*8 bytes)
|
||||
// Gets or sets the GNSS system channel sharing configuration. If the receiver is sent a valid new configuration, it will respond with a UBX-ACK- ACK message and immediately change to the new configuration. Otherwise the receiver will reject the request, by issuing a UBX-ACK-NAK and continuing operation with the previous configuration. Configuration requirements: It is necessary for at least one major GNSS to be enabled, after applying the new configuration to the current one. It is also required that at least 4 tracking channels are available to each enabled major GNSS, i.e. maxTrkCh must have a minimum value of 4 for each enabled major GNSS. The number of tracking channels in use must not exceed the number of tracking channels available in hardware, and the sum of all reserved tracking channels needs to be less than or equal to the number of tracking channels in use. Notes: To avoid cross-correlation issues, it is recommended that GPS and QZSS are always both enabled or both disabled. Polling this message returns the configuration of all supported GNSS, whether enabled or not; it may also include GNSS unsupported by the particular product, but in such cases the enable flag will always be unset. See section GNSS Configuration for a discussion of the use of this message. See section Satellite Numbering for a description of the GNSS IDs available. Configuration specific to the GNSS system can be done via other messages (e. g. UBX-CFG-SBAS).
|
||||
type CfgGnss struct {
|
||||
MsgVer byte // Message version (0x00 for this version)
|
||||
NumTrkChHw byte // Number of tracking channels available in hardware (read only)
|
||||
NumTrkChUse byte // (Read only in protocol versions greater than 23) Number of tracking channels to use. Must be > 0, <= numTrkChHw. If 0xFF, then number of tracking channels to use will be set to numTrkChHw.
|
||||
NumConfigBlocks byte `len:"ConfigBlocks"` // Number of configuration blocks following
|
||||
ConfigBlocks []CfgGnssConfigBlocksType // len: NumConfigBlocks
|
||||
}
|
||||
|
||||
func (CfgGnss) classID() uint16 { return 0x3e06 }
|
||||
|
||||
type CfgGnssConfigBlocksType struct {
|
||||
GnssId byte // System identifier (see Satellite Numbering )
|
||||
ResTrkCh byte // (Read only in protocol versions greater than 23) Number of reserved (minimum) tracking channels for this system.
|
||||
MaxTrkCh byte // (Read only in protocol versions greater than 23) Maximum number of tracking channels used for this system. Must be > 0, >= resTrkChn, <= numTrkChUse and <= maximum number of tracking channels supported for this system.
|
||||
Reserved1 byte // Reserved
|
||||
Flags CfgGnssFlags // Bitfield of flags. At least one signal must be configured in every enabled system.
|
||||
}
|
||||
|
||||
type CfgGnssFlags uint32
|
||||
|
||||
const (
|
||||
CfgGnssEnable CfgGnssFlags = 0x1 // Enable this system
|
||||
CfgGnssSigCfgMask CfgGnssFlags = 0xff0000 // Signal configuration mask When gnssId is 0 (GPS) 0x01 = GPS L1C/A 0x10 = GPS L2C 0x20 = GPS L5 When gnssId is 1 (SBAS) 0x01 = SBAS L1C/A When gnssId is 2 (Galileo) 0x01 = Galileo E1 (not supported in protocol versions less than 18) 0x10 = Galileo E5a 0x20 = Galileo E5b When gnssId is 3 (BeiDou) 0x01 = BeiDou B1I 0x10 = BeiDou B2I 0x80 = BeiDou B2A When gnssId is 4 (IMES) 0x01 = IMES L1 When gnssId is 5 (QZSS) 0x01 = QZSS L1C/A 0x04 = QZSS L1S 0x10 = QZSS L2C 0x20 = QZSS L5 When gnssId is 6 (GLONASS) 0x01 = GLONASS L1 0x10 = GLONASS L2
|
||||
)
|
||||
|
||||
// Write CfgGnss message to buffer
|
||||
func (cfg CfgGnss) Put(buf []byte) error {
|
||||
sz := cfg.Size()
|
||||
if sz > len(buf) {
|
||||
return io.ErrShortBuffer
|
||||
}
|
||||
copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 4 + byte(len(cfg.ConfigBlocks))*8, 0})
|
||||
buf[6] = cfg.MsgVer
|
||||
buf[7] = cfg.NumTrkChHw
|
||||
buf[8] = cfg.NumTrkChUse
|
||||
buf[9] = byte(len(cfg.ConfigBlocks))
|
||||
offset := 10
|
||||
for _, block := range cfg.ConfigBlocks {
|
||||
buf[offset] = block.GnssId
|
||||
buf[offset+1] = block.ResTrkCh
|
||||
buf[offset+2] = block.MaxTrkCh
|
||||
buf[offset+3] = block.Reserved1
|
||||
buf[offset+4] = byte(block.Flags)
|
||||
buf[offset+5] = byte(block.Flags >> 8)
|
||||
buf[offset+6] = byte(block.Flags >> 16)
|
||||
buf[offset+7] = byte(block.Flags >> 24)
|
||||
offset += 8
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns length of CfgGnss in bytes when sent over the wire.
|
||||
func (cfg CfgGnss) Size() int {
|
||||
return 10 + 8*len(cfg.ConfigBlocks)
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package gps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCfgNav5ClassID(t *testing.T) {
|
||||
cfg := CfgNav5{}
|
||||
if got := cfg.classID(); got != 0x2406 {
|
||||
t.Errorf("expected 0x2406, got 0x%04x", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgNav5Write(t *testing.T) {
|
||||
cfg := CfgNav5{
|
||||
Mask: CfgNav5Dyn | CfgNav5MinEl,
|
||||
DynModel: 4,
|
||||
FixMode: 3,
|
||||
FixedAlt_me2: 10000,
|
||||
FixedAltVar_m2e4: 10000,
|
||||
MinElev_deg: 5,
|
||||
DrLimit_s: 0,
|
||||
PDop: 250,
|
||||
TDop: 250,
|
||||
PAcc_m: 100,
|
||||
TAcc_m: 300,
|
||||
StaticHoldThresh_cm_s: 50,
|
||||
DgnssTimeout_s: 60,
|
||||
CnoThreshNumSVs: 3,
|
||||
CnoThresh_dbhz: 35,
|
||||
Reserved1: [2]byte{0, 0},
|
||||
StaticHoldMaxDist_m: 200,
|
||||
UtcStandard: 0,
|
||||
Reserved2: [5]byte{0, 0, 0, 0, 0},
|
||||
}
|
||||
|
||||
buf := make([]byte, 64)
|
||||
cfg.Put42Bytes(buf)
|
||||
|
||||
// Check sync chars
|
||||
if buf[0] != 0xb5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync chars 0xb5 0x62, got 0x%02x 0x%02x", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Check class/id (little-endian)
|
||||
if buf[2] != 0x06 || buf[3] != 0x24 {
|
||||
t.Errorf("expected class/id 0x06 0x24, got 0x%02x 0x%02x", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Check length
|
||||
if buf[4] != 36 || buf[5] != 0 {
|
||||
t.Errorf("expected length 36, got %d", uint16(buf[4])|uint16(buf[5])<<8)
|
||||
}
|
||||
|
||||
// Check Mask (little-endian)
|
||||
mask := uint16(buf[6]) | uint16(buf[7])<<8
|
||||
if mask != uint16(CfgNav5Dyn|CfgNav5MinEl) {
|
||||
t.Errorf("expected mask 0x03, got 0x%04x", mask)
|
||||
}
|
||||
|
||||
// Check DynModel
|
||||
if buf[8] != 4 {
|
||||
t.Errorf("expected DynModel 4, got %d", buf[8])
|
||||
}
|
||||
|
||||
// Check FixMode
|
||||
if buf[9] != 3 {
|
||||
t.Errorf("expected FixMode 3, got %d", buf[9])
|
||||
}
|
||||
|
||||
// Check FixedAlt_me2 (little-endian int32)
|
||||
fixedAlt := int32(buf[10]) | int32(buf[11])<<8 | int32(buf[12])<<16 | int32(buf[13])<<24
|
||||
if fixedAlt != 10000 {
|
||||
t.Errorf("expected FixedAlt_me2 10000, got %d", fixedAlt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgGnssClassID(t *testing.T) {
|
||||
cfg := CfgGnss{}
|
||||
if got := cfg.classID(); got != 0x3e06 {
|
||||
t.Errorf("expected 0x3e06, got 0x%04x", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgGnssWrite(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cfg CfgGnss
|
||||
expectedLen int
|
||||
expectedBlocks byte
|
||||
}{
|
||||
{
|
||||
name: "no config blocks",
|
||||
cfg: CfgGnss{
|
||||
MsgVer: 0,
|
||||
NumTrkChHw: 32,
|
||||
NumTrkChUse: 32,
|
||||
ConfigBlocks: nil,
|
||||
},
|
||||
expectedLen: 10,
|
||||
expectedBlocks: 0,
|
||||
},
|
||||
{
|
||||
name: "one config block",
|
||||
cfg: CfgGnss{
|
||||
MsgVer: 0,
|
||||
NumTrkChHw: 32,
|
||||
NumTrkChUse: 32,
|
||||
ConfigBlocks: []CfgGnssConfigBlocksType{
|
||||
{GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000},
|
||||
},
|
||||
},
|
||||
expectedLen: 18,
|
||||
expectedBlocks: 1,
|
||||
},
|
||||
{
|
||||
name: "two config blocks",
|
||||
cfg: CfgGnss{
|
||||
MsgVer: 0,
|
||||
NumTrkChHw: 32,
|
||||
NumTrkChUse: 32,
|
||||
ConfigBlocks: []CfgGnssConfigBlocksType{
|
||||
{GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000},
|
||||
{GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: CfgGnssEnable | 0x010000},
|
||||
},
|
||||
},
|
||||
expectedLen: 26,
|
||||
expectedBlocks: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
buf := make([]byte, 64)
|
||||
err := tc.cfg.Put(buf)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error, data too long?: %v", err)
|
||||
}
|
||||
// Check sync chars
|
||||
if buf[0] != 0xb5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync chars 0xb5 0x62, got 0x%02x 0x%02x", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Check class/id (little-endian)
|
||||
if buf[2] != 0x06 || buf[3] != 0x3e {
|
||||
t.Errorf("expected class/id 0x06 0x3e, got 0x%02x 0x%02x", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Check number of config blocks
|
||||
if buf[9] != tc.expectedBlocks {
|
||||
t.Errorf("expected %d config blocks, got %d", tc.expectedBlocks, buf[9])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgGnssWriteBlockContent(t *testing.T) {
|
||||
cfg := CfgGnss{
|
||||
MsgVer: 0,
|
||||
NumTrkChHw: 32,
|
||||
NumTrkChUse: 32,
|
||||
ConfigBlocks: []CfgGnssConfigBlocksType{
|
||||
{GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Reserved1: 0, Flags: CfgGnssEnable | 0x010000},
|
||||
},
|
||||
}
|
||||
|
||||
buf := make([]byte, 64)
|
||||
err := cfg.Put(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Check first block at offset 10
|
||||
if buf[10] != 0 {
|
||||
t.Errorf("expected GnssId 0, got %d", buf[10])
|
||||
}
|
||||
if buf[11] != 8 {
|
||||
t.Errorf("expected ResTrkCh 8, got %d", buf[11])
|
||||
}
|
||||
if buf[12] != 16 {
|
||||
t.Errorf("expected MaxTrkCh 16, got %d", buf[12])
|
||||
}
|
||||
|
||||
// Check flags (little-endian uint32)
|
||||
flags := uint32(buf[14]) | uint32(buf[15])<<8 | uint32(buf[16])<<16 | uint32(buf[17])<<24
|
||||
expectedFlags := uint32(CfgGnssEnable | 0x010000)
|
||||
if flags != expectedFlags {
|
||||
t.Errorf("expected flags 0x%08x, got 0x%08x", expectedFlags, flags)
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -210,7 +210,7 @@ func (d *Device) SendCommand(command byte) {
|
||||
d.bus.SetCommandMode(true)
|
||||
d.bus.Write([]byte{command})
|
||||
|
||||
for d.busy(command == DISPLAY_CLEAR || command == CURSOR_HOME) {
|
||||
for d.isBusy(command == DISPLAY_CLEAR || command == CURSOR_HOME) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func (d *Device) sendData(data byte) {
|
||||
d.bus.SetCommandMode(false)
|
||||
d.bus.Write([]byte{data})
|
||||
|
||||
for d.busy(false) {
|
||||
for d.isBusy(false) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,9 +231,9 @@ func (d *Device) CreateCharacter(cgramAddr uint8, data []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// busy returns true when hd447890 is busy
|
||||
// isBusy returns true when hd447890 is isBusy
|
||||
// or after the timeout specified
|
||||
func (d *Device) busy(longDelay bool) bool {
|
||||
func (d *Device) isBusy(longDelay bool) bool {
|
||||
if d.bus.WriteOnly() {
|
||||
// Can't read busy flag if write only, so sleep a bit then return
|
||||
if longDelay {
|
||||
@@ -261,7 +261,7 @@ func (d *Device) busy(longDelay bool) bool {
|
||||
|
||||
// Busy returns true when hd447890 is busy
|
||||
func (d *Device) Busy() bool {
|
||||
return d.busy(false)
|
||||
return d.isBusy(false)
|
||||
}
|
||||
|
||||
// Size returns the current size of the display.
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package honeyhsc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
var (
|
||||
errSensorMissing = errors.New("hsc: not connected")
|
||||
errDiagnostic = errors.New("hsc: diagnostic error")
|
||||
)
|
||||
|
||||
const (
|
||||
measuremask = drivers.Pressure | drivers.Temperature
|
||||
statusMask = 0b1100_0000
|
||||
statusOffset = 6
|
||||
)
|
||||
|
||||
// DevI2C is the TruStability® High Accuracy Silicon Ceramic (HSC) Series is a piezoresistive silicon pressure sensor offering a ratiometric
|
||||
// analog or digital output for reading pressure over the specified full scale pressure span and temperature range.
|
||||
type DevI2C struct {
|
||||
bus drivers.I2C
|
||||
dev
|
||||
addr uint8
|
||||
buf [6]byte
|
||||
}
|
||||
|
||||
// NewDevI2C creates and returns a new DevI2C that communicates with an HSC device over the provided I2C bus.
|
||||
// Parameters:
|
||||
// - bus: the I2C bus to use.
|
||||
// - addr: the 7-bit I2C address of the sensor.
|
||||
// - outMin, outMax: raw output code range (counts) corresponding to the pressure span. Depends on sensor model.
|
||||
// - pMin, pMax: pressure range endpoints in millipascals (mPa). Depends on sensor model.
|
||||
//
|
||||
// The returned DevI2C will use these calibration parameters to convert raw bridge counts to pressure.
|
||||
func NewDevI2C(bus drivers.I2C, addr, outMin, outMax uint16, pMin, pMax int32) *DevI2C {
|
||||
h := &DevI2C{
|
||||
bus: bus,
|
||||
addr: uint8(addr),
|
||||
dev: dev{
|
||||
cmin: outMin,
|
||||
cmax: outMax,
|
||||
pmin: pMin,
|
||||
pmax: pMax,
|
||||
},
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// ReadTemperature reads and returns the temperature in milliKelvin (mC) from the I2C-attached HSC device.
|
||||
// It performs an Update internally to get the latest temperature value.
|
||||
func (h *DevI2C) ReadTemperature() (int32, error) {
|
||||
err := h.Update(drivers.Temperature)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return h.Temperature(), nil
|
||||
}
|
||||
|
||||
// Update reads both temperature and pressure data from the I2C-attached HSC device when
|
||||
// the requested measurement mask includes pressure or temperature.
|
||||
// If neither pressure nor temperature is requested, Update is a no-op.
|
||||
func (d *DevI2C) Update(which drivers.Measurement) error {
|
||||
// Update performs an I2C transaction to read 4 bytes, parses the status bits, 14-bit bridge data and
|
||||
// temperature bits, and forwards them to the internal update routine. Any I2C transport error is returned,
|
||||
// as well as errors produced by the internal update (e.g. errSensorMissing, errDiagnostic).
|
||||
if which&measuremask == 0 {
|
||||
return nil
|
||||
}
|
||||
rbuf := d.buf[:4]
|
||||
wbuf := d.buf[4:6]
|
||||
const reg = 0
|
||||
value := (d.addr << 1) | 1
|
||||
wbuf[0] = reg
|
||||
wbuf[1] = value
|
||||
err := d.bus.Tx(uint16(d.addr), wbuf, rbuf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status := (rbuf[0] & statusMask) >> statusOffset
|
||||
bridgeData := (uint16(rbuf[0]&^statusMask) << 8) | uint16(rbuf[1])
|
||||
tempData := uint16(rbuf[2])<<8 | uint16(rbuf[3]&0xe0)>>5
|
||||
return d.dev.update(status, bridgeData, tempData)
|
||||
}
|
||||
|
||||
type pinout func(level bool)
|
||||
|
||||
// DevI2C is the TruStability® High Accuracy Silicon Ceramic (HSC) Series is a piezoresistive silicon pressure sensor offering a ratiometric
|
||||
// analog or digital output for reading pressure over the specified full scale pressure span and temperature range.
|
||||
type DevSPI struct {
|
||||
spi drivers.SPI
|
||||
cs pinout
|
||||
dev
|
||||
buf [4]byte
|
||||
}
|
||||
|
||||
// NewDevSPI creates and returns a new DevSPI that communicates with an HSC device over SPI.
|
||||
// Parameters:
|
||||
// - conn: the SPI connection to use.
|
||||
// - cs: a chip-select function that drives the device select line low/high.
|
||||
// - outMin, outMax: raw output code range (counts) corresponding to the pressure span. Depends on sensor model.
|
||||
// - pMin, pMax: pressure range endpoints in millipascals (mPa). Depends on sensor model.
|
||||
//
|
||||
// The function returns the constructed DevSPI and an error value (currently always nil).
|
||||
func NewDevSPI(conn drivers.SPI, cs pinout, outMin, outMax uint16, pMin, pMax int32) (*DevSPI, error) {
|
||||
h := &DevSPI{
|
||||
spi: conn,
|
||||
cs: cs,
|
||||
dev: dev{
|
||||
cmin: outMin,
|
||||
cmax: outMax,
|
||||
pmin: pMin,
|
||||
pmax: pMax,
|
||||
},
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// ReadTemperature reads and returns the temperature in milliKelvin (mC) from the SPI-attached HSC device.
|
||||
// It performs an Update internally to get the latest temperature value.
|
||||
func (h *DevSPI) ReadTemperature() (int32, error) {
|
||||
err := h.Update(drivers.Temperature)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return h.Temperature(), nil
|
||||
}
|
||||
|
||||
// Update reads pressure and temperature data from the SPI-attached HSC device when the requested measurement mask includes
|
||||
// pressure or temperature. If neither pressure nor temperature is requested, Update is a no-op.
|
||||
func (h *DevSPI) Update(which drivers.Measurement) error {
|
||||
// It toggles the provided chip-select, performs an SPI transfer to read 4 bytes, parses the status bits,
|
||||
// 14-bit bridge data and temperature bits, and forwards them to the internal update routine. Any SPI
|
||||
// transport error is returned, as well as errors produced by the internal update (e.g. errSensorMissing, errDiagnostic).
|
||||
if which&measuremask == 0 {
|
||||
return nil
|
||||
}
|
||||
buf := &h.buf
|
||||
h.cs(false)
|
||||
err := h.spi.Tx(nil, buf[:4])
|
||||
h.cs(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// First two bits are status bits.
|
||||
status := (buf[0] & statusMask) >> statusOffset
|
||||
bridgeData := (uint16(buf[0]&^statusMask) << 8) | uint16(buf[1])
|
||||
|
||||
tempData := uint16(buf[2])<<8 | uint16(buf[3]&0xe0)>>5
|
||||
return h.dev.update(status, bridgeData, tempData)
|
||||
}
|
||||
|
||||
type dev struct {
|
||||
pressure int32
|
||||
temp int32
|
||||
cmin, cmax uint16
|
||||
pmin, pmax int32
|
||||
}
|
||||
|
||||
// Pressure returns the most recently computed pressure value in millipascals (mPa).
|
||||
// The value is taken from the last successful Update.
|
||||
func (d *dev) Pressure() int32 {
|
||||
return d.pressure
|
||||
}
|
||||
|
||||
// Temperature returns the most recently read temperature value in milliKelvin (mC).
|
||||
// The value is taken from the last successful Update.
|
||||
func (d *dev) Temperature() int32 {
|
||||
return d.temp + 273_150
|
||||
}
|
||||
|
||||
// update interprets raw sensor fields (status, bridgeData, tempData) and updates the dev's stored
|
||||
// pressure and temperature. It returns errSensorMissing when the temperature raw value indicates no sensor
|
||||
// (tempData == math.MaxUint16), errDiagnostic when the status indicates a device diagnostic condition
|
||||
// (status == 3), or nil on success. Pressure is computed with integer arithmetic using the configured
|
||||
// cmin/cmax -> pmin/pmax linear mapping in order to avoid overflows.
|
||||
func (d *dev) update(status uint8, bridgeData, tempData uint16) error {
|
||||
if tempData == math.MaxUint16 {
|
||||
return errSensorMissing
|
||||
} else if status == 3 {
|
||||
return errDiagnostic
|
||||
}
|
||||
|
||||
// Take care not to overflow here.
|
||||
p := (int32(bridgeData)-int32(d.cmin))*(d.pmax-d.pmin)/int32(d.cmax-d.cmin) + d.pmin
|
||||
d.temp = int32(tempData)
|
||||
d.pressure = p
|
||||
return nil
|
||||
}
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
)
|
||||
|
||||
type spiDriver struct {
|
||||
bus machine.SPI
|
||||
bus *machine.SPI
|
||||
}
|
||||
|
||||
func NewSPI(bus machine.SPI, dc, cs, rst machine.Pin) *Device {
|
||||
func NewSPI(bus *machine.SPI, dc, cs, rst machine.Pin) *Device {
|
||||
return &Device{
|
||||
dc: dc,
|
||||
cs: cs,
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
)
|
||||
|
||||
type spiDriver struct {
|
||||
bus machine.SPI
|
||||
bus *machine.SPI
|
||||
}
|
||||
|
||||
func NewSPI(bus machine.SPI, dc, cs, rst machine.Pin) *Device {
|
||||
func NewSPI(bus *machine.SPI, dc, cs, rst machine.Pin) *Device {
|
||||
return &Device{
|
||||
dc: dc,
|
||||
cs: cs,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// The pingconfig group of files serve to abstract away
|
||||
// pin configuration calls on the machine.Pin type.
|
||||
// It was observed this way of developing drivers was
|
||||
// non-portable and unusable on "big" Go projects so
|
||||
// future projects should NOT configure pins in driver code.
|
||||
// Users must configure pins before passing them as arguments
|
||||
// to drivers.
|
||||
|
||||
// ConfigurePinOut is a legacy function used to configure pins as outputs.
|
||||
//
|
||||
// Deprecated: Do not configure pins in drivers.
|
||||
// This is a legacy feature and should only be used by drivers that
|
||||
// previously configured pins in initialization to avoid breaking users.
|
||||
func ConfigurePinOut(po pin.Output) {
|
||||
configurePinOut(po)
|
||||
}
|
||||
|
||||
// ConfigurePinInput is a legacy function used to configure pins as inputs.
|
||||
//
|
||||
// Deprecated: Do not configure pins in drivers.
|
||||
// This is a legacy feature and should only be used by drivers that
|
||||
// previously configured pins in initialization to avoid breaking users.
|
||||
func ConfigurePinInputPulldown(pi pin.Input) {
|
||||
configurePinInputPulldown(pi)
|
||||
}
|
||||
|
||||
// ConfigurePinInput is a legacy function used to configure pins as inputs.
|
||||
//
|
||||
// Deprecated: Do not configure pins in drivers.
|
||||
// This is a legacy feature and should only be used by drivers that
|
||||
// previously configured pins in initialization to avoid breaking users.
|
||||
func ConfigurePinInput(pi pin.Input) {
|
||||
configurePinInput(pi)
|
||||
}
|
||||
|
||||
// ConfigurePinInput is a legacy function used to configure pins as inputs.
|
||||
//
|
||||
// Deprecated: Do not configure pins in drivers.
|
||||
// This is a legacy feature and should only be used by drivers that
|
||||
// previously configured pins in initialization to avoid breaking users.
|
||||
func ConfigurePinInputPullup(pi pin.Input) {
|
||||
configurePinInputPullup(pi)
|
||||
}
|
||||
|
||||
// PinIsNoPin returns true if the argument is a machine.Pin type and is the machine.NoPin predeclared type.
|
||||
//
|
||||
// Deprecated: Drivers do not require pin knowledge from now on.
|
||||
func PinIsNoPin(pin any) bool {
|
||||
return pinIsNoPin(pin)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrConfigBeforeInstantiated = errors.New("device must be instantiated with New before calling Configure method")
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build !tinygo
|
||||
|
||||
package legacy
|
||||
|
||||
import "tinygo.org/x/drivers/internal/pin"
|
||||
|
||||
// This file compiles for non-tinygo builds
|
||||
// for use with "big" or "upstream" Go where
|
||||
// there is no machine package.
|
||||
|
||||
func configurePinOut(p pin.Output) {}
|
||||
func configurePinInput(p pin.Input) {}
|
||||
func configurePinInputPulldown(p pin.Input) {}
|
||||
func configurePinInputPullup(p pin.Input) {}
|
||||
func pinIsNoPin(a any) bool { return false }
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build baremetal && fe310
|
||||
|
||||
package legacy
|
||||
|
||||
import "machine"
|
||||
|
||||
const (
|
||||
pulldown = machine.PinInput
|
||||
pullup = machine.PinInput
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build baremetal && !fe310
|
||||
|
||||
package legacy
|
||||
|
||||
import "machine"
|
||||
|
||||
// If you are getting a build error here you then we missed adding
|
||||
// your CPU build tag to the list of CPUs that do not have pulldown/pullups.
|
||||
// Add it above and in pinhal_nopulls! You should also add a smoketest for it :)
|
||||
const (
|
||||
pulldown = machine.PinInputPulldown
|
||||
pullup = machine.PinInputPullup
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
//go:build baremetal
|
||||
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
func configurePinOut(po pin.Output) {
|
||||
configurePin(po, machine.PinOutput)
|
||||
}
|
||||
|
||||
func configurePinInputPulldown(pi pin.Input) {
|
||||
configurePin(pi, pulldown) // some chips do not have pull down, in which case pulldown==machine.PinInput.
|
||||
}
|
||||
|
||||
func configurePinInput(pi pin.Input) {
|
||||
configurePin(pi, machine.PinInput)
|
||||
}
|
||||
|
||||
func configurePinInputPullup(pi pin.Input) {
|
||||
configurePin(pi, pullup) // some chips do not have pull up, in which case pullup==machine.PinInput.
|
||||
}
|
||||
|
||||
func pinIsNoPin(a any) bool {
|
||||
p, ok := a.(machine.Pin)
|
||||
return ok && p == machine.NoPin
|
||||
}
|
||||
|
||||
func configurePin(p any, mode machine.PinMode) {
|
||||
machinePin, ok := p.(machine.Pin)
|
||||
if ok {
|
||||
machinePin.Configure(machine.PinConfig{Mode: mode})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// package pin implements a TinyGo Pin HAL.
|
||||
// It serves to eliminate machine.Pin from driver constructors
|
||||
// so that drivers can be used in "big" Go projects where
|
||||
// there is no machine package.
|
||||
// This file contains both function and interface-style Pin HAL definitions.
|
||||
package pin
|
||||
|
||||
// OutputFunc is hardware abstraction for a pin which outputs a
|
||||
// digital signal (high or low level).
|
||||
//
|
||||
// // Code conversion demo: from machine.Pin to pin.OutputFunc
|
||||
// led := machine.LED
|
||||
// led.Configure(machine.PinConfig{Mode: machine.Output})
|
||||
// var pin pin.OutputFunc = led.Set // Going from a machine.Pin to a pin.OutputFunc
|
||||
//
|
||||
// This is an alternative to [Output] which is an interface type.
|
||||
type OutputFunc func(level bool)
|
||||
|
||||
// High sets the underlying pin's level to high. This is equivalent to calling PinOutput(true).
|
||||
func (setPin OutputFunc) High() {
|
||||
setPin(true)
|
||||
}
|
||||
|
||||
// Low sets the underlying pin's level to low. This is equivalent to calling PinOutput(false).
|
||||
func (setPin OutputFunc) Low() {
|
||||
setPin(false)
|
||||
}
|
||||
|
||||
// InputFunc is hardware abstraction for a pin which receives a
|
||||
// digital signal and reads it (high or low level).
|
||||
//
|
||||
// // Code conversion demo: from machine.Pin to pin.InputFunc
|
||||
// input := machine.LED
|
||||
// input.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) // or use machine.PinInputPullup or machine.Input
|
||||
// var pin pin.InputFunc = input.Get // Going from a machine.Pin to a pin.InputFunc
|
||||
//
|
||||
// This is an alternative to [Input] which is an interface type.
|
||||
type InputFunc func() (level bool)
|
||||
|
||||
// // Below is an example on how to define a input/output pin HAL for a
|
||||
// // pin that must switch between input and output mode:
|
||||
//
|
||||
// var pinIsOutput bool
|
||||
// var po PinOutputFunc = func(b bool) {
|
||||
// if !pinIsOutput {
|
||||
// pin.Configure(outputMode)
|
||||
// pinIsOutput = true
|
||||
// }
|
||||
// pin.Set(b)
|
||||
// }
|
||||
//
|
||||
// var pi PinInputFunc = func() bool {
|
||||
// if pinIsOutput {
|
||||
// pin.Configure(inputMode)
|
||||
// pinIsOutput = false
|
||||
// }
|
||||
// return pin.Get()
|
||||
// }
|
||||
|
||||
// Output interface represents a pin hardware abstraction layer for a pin that can output a digital signal.
|
||||
//
|
||||
// This is an alternative to [OutputFunc] abstraction which is a function type.
|
||||
type Output interface {
|
||||
Set(level bool)
|
||||
}
|
||||
|
||||
// Input interface represents a pin hardware abstraction layer for a pin that can read a digital signal.
|
||||
//
|
||||
// This is an alternative to [InputFunc] abstraction which is a function type.
|
||||
type Input interface {
|
||||
Get() (level bool)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package regmap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
// Device8 implements common logic to most 8-bit peripherals with an I2C or SPI bus.
|
||||
// All methods expect the target to support conventional register read and write operations
|
||||
// where the first byte sent is the register address being accessed.
|
||||
//
|
||||
// All methods use an internal buffer and perform no dynamic memory allocation.
|
||||
type Device8 struct {
|
||||
buf [10]byte
|
||||
}
|
||||
|
||||
// clear zeroes Device8's buffers.
|
||||
func (d *Device8) clear() {
|
||||
d.buf = [10]byte{}
|
||||
}
|
||||
|
||||
// I2C methods.
|
||||
|
||||
// Read8I2C reads a single byte from register addr of the device at i2cAddr using the provided I2C bus.
|
||||
func (d *Device8) Read8I2C(bus drivers.I2C, i2cAddr uint16, addr uint8) (byte, error) {
|
||||
d.buf[0] = addr
|
||||
err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:2])
|
||||
return d.buf[1], err
|
||||
}
|
||||
|
||||
// Read16I2C reads a 16-bit value from register addr of the device at i2cAddr using the provided I2C bus.
|
||||
// The byte order is specified by order.
|
||||
func (d *Device8) Read16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint16, error) {
|
||||
d.buf[0] = addr
|
||||
err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:3])
|
||||
return order.Uint16(d.buf[1:3]), err
|
||||
}
|
||||
|
||||
// Read32I2C reads a 32-bit value from register addr of the device at i2cAddr using the provided I2C bus.
|
||||
// The byte order is specified by order.
|
||||
func (d *Device8) Read32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint32, error) {
|
||||
d.buf[0] = addr
|
||||
err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:5])
|
||||
return order.Uint32(d.buf[1:5]), err
|
||||
}
|
||||
|
||||
// ReadDataI2C reads dataLength bytes from register addr of the device at i2cAddr using the provided I2C bus.
|
||||
// The data is stored in dataDestination.
|
||||
func (d *Device8) ReadDataI2C(bus drivers.I2C, i2cAddr uint16, addr uint8, dataDestination []byte) error {
|
||||
d.buf[0] = addr
|
||||
return bus.Tx(i2cAddr, d.buf[:1], dataDestination)
|
||||
}
|
||||
|
||||
// Write8I2C writes a single byte value to register addr of the device at i2cAddr using the provided I2C bus.
|
||||
func (d *Device8) Write8I2C(bus drivers.I2C, i2cAddr uint16, addr, value uint8) error {
|
||||
d.buf[0] = addr
|
||||
d.buf[1] = value
|
||||
return bus.Tx(i2cAddr, d.buf[:2], nil)
|
||||
}
|
||||
|
||||
// Write16I2C writes a 16-bit value to register addr of the device at i2cAddr using the provided I2C bus.
|
||||
// The byte order is specified by order.
|
||||
func (d *Device8) Write16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint16, order binary.ByteOrder) error {
|
||||
d.buf[0] = addr
|
||||
order.PutUint16(d.buf[1:3], value)
|
||||
return bus.Tx(i2cAddr, d.buf[0:3], nil)
|
||||
}
|
||||
|
||||
// Write32I2C writes a 32-bit value to register addr of the device at i2cAddr using the provided I2C bus.
|
||||
// The byte order is specified by order.
|
||||
func (d *Device8) Write32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint32, order binary.ByteOrder) error {
|
||||
d.buf[0] = addr
|
||||
order.PutUint32(d.buf[1:5], value)
|
||||
return bus.Tx(i2cAddr, d.buf[0:5], nil)
|
||||
}
|
||||
|
||||
// SPI methods.
|
||||
|
||||
// Read8SPI reads a single byte from register addr using the provided SPI bus.
|
||||
func (d *Device8) Read8SPI(bus drivers.SPI, addr uint8) (byte, error) {
|
||||
d.clear()
|
||||
d.buf[0] = addr
|
||||
err := bus.Tx(d.buf[0:1], d.buf[1:2]) // We suppose data is returned after first byte in SPI.
|
||||
return d.buf[1], err
|
||||
}
|
||||
|
||||
// Read16SPI reads a 16-bit value from register addr using the provided SPI bus. The byte order is specified by order.
|
||||
func (d *Device8) Read16SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint16, error) {
|
||||
d.clear()
|
||||
d.buf[0] = addr
|
||||
err := bus.Tx(d.buf[0:3], d.buf[3:6]) // We suppose data is returned after first byte in SPI.
|
||||
return order.Uint16(d.buf[4:6]), err
|
||||
}
|
||||
|
||||
// Read32SPI reads a 32-bit value from register addr using the provided SPI bus. The byte order is specified by order.
|
||||
func (d *Device8) Read32SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint32, error) {
|
||||
d.clear()
|
||||
d.buf[0] = addr
|
||||
err := bus.Tx(d.buf[0:5], d.buf[5:10]) // We suppose data is returned after first byte in SPI.
|
||||
return order.Uint32(d.buf[6:10]), err
|
||||
}
|
||||
|
||||
// ReadDataSPI reads data from a 8bit device address. It assumes data at register address is sent back
|
||||
// from device after first byte is written as address.
|
||||
// It needs the auxiliary buffer length to be large enough to contain both the write and read portions of buffer,
|
||||
// so 2*(dataLength+1) < len(auxiliaryBuf) must hold.
|
||||
func (d *Device8) ReadDataSPI(bus drivers.SPI, addr uint8, dataLength int, auxiliaryBuf []byte) ([]byte, error) {
|
||||
split := len(auxiliaryBuf) / 2
|
||||
if split < dataLength+1 {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
wbuf, rbuf := auxiliaryBuf[:split], auxiliaryBuf[split:]
|
||||
wbuf[0] = addr
|
||||
err := bus.Tx(wbuf, rbuf)
|
||||
return rbuf[1:], err
|
||||
}
|
||||
|
||||
// Write8SPI writes a single byte value to register addr using the provided SPI bus.
|
||||
func (d *Device8) Write8SPI(bus drivers.SPI, addr, value uint8) error {
|
||||
d.clear()
|
||||
d.buf[0] = addr
|
||||
d.buf[1] = value
|
||||
return bus.Tx(d.buf[:2], nil)
|
||||
}
|
||||
|
||||
// Write16SPI writes a 16-bit value to register addr using the provided SPI bus. The byte order is specified by order.
|
||||
func (d *Device8) Write16SPI(bus drivers.SPI, addr uint8, value uint16, order binary.ByteOrder) error {
|
||||
d.clear()
|
||||
d.buf[0] = addr
|
||||
order.PutUint16(d.buf[1:3], value)
|
||||
return bus.Tx(d.buf[:3], nil)
|
||||
}
|
||||
|
||||
// Write32SPI writes a 32-bit value to register addr using the provided SPI bus. The byte order is specified by order.
|
||||
func (d *Device8) Write32SPI(bus drivers.SPI, addr uint8, value uint32, order binary.ByteOrder) error {
|
||||
d.clear()
|
||||
d.buf[0] = addr
|
||||
order.PutUint32(d.buf[1:5], value)
|
||||
return bus.Tx(d.buf[:5], nil)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package regmap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
// Device8SPI implements common logic to most 8-bit peripherals with an SPI bus.
|
||||
// All methods expect the target to support conventional register read and write operations
|
||||
// where the first byte sent is the register address being accessed.
|
||||
//
|
||||
// All methods use an internal buffer and perform no dynamic memory allocation.
|
||||
type Device8SPI struct {
|
||||
bus drivers.SPI
|
||||
order binary.ByteOrder
|
||||
d Device8
|
||||
}
|
||||
|
||||
// SetBus sets the SPI bus and byte order for the Device8SPI.
|
||||
//
|
||||
// As a hint, most SPI devices use big-endian (MSB) byte order.
|
||||
// - Big endian: A value of 0x1234 is transmitted as 0x12 followed by 0x34.
|
||||
// - Little endian: A value of 0x1234 is transmitted as 0x34 followed by 0x12.
|
||||
func (d *Device8SPI) SetBus(bus drivers.SPI, order binary.ByteOrder) {
|
||||
d.bus = bus
|
||||
d.order = order
|
||||
}
|
||||
|
||||
// Read8 reads a single byte from register addr.
|
||||
func (d *Device8SPI) Read8(addr uint8) (byte, error) {
|
||||
return d.d.Read8SPI(d.bus, addr)
|
||||
}
|
||||
|
||||
// Read16 reads a 16-bit value from register addr.
|
||||
func (d *Device8SPI) Read16(addr uint8) (uint16, error) {
|
||||
return d.d.Read16SPI(d.bus, addr, d.order)
|
||||
}
|
||||
|
||||
// Read32 reads a 32-bit value from register addr.
|
||||
func (d *Device8SPI) Read32(addr uint8) (uint32, error) {
|
||||
return d.d.Read32SPI(d.bus, addr, d.order)
|
||||
}
|
||||
|
||||
// ReadData reads dataLength bytes from register addr. Due to the internal functioning of
|
||||
// SPI, an auxiliary buffer must be provided to perform the operation and avoid memory allocation.
|
||||
// The returned slice is a subslice of auxBuffer containing the read data.
|
||||
func (d *Device8SPI) ReadData(addr uint8, datalength int, auxBuffer []byte) ([]byte, error) {
|
||||
return d.d.ReadDataSPI(d.bus, addr, datalength, auxBuffer)
|
||||
}
|
||||
|
||||
// Write8 writes a single byte value to register addr.
|
||||
func (d *Device8SPI) Write8(addr, value uint8) error {
|
||||
return d.d.Write8SPI(d.bus, addr, value)
|
||||
}
|
||||
|
||||
// Write16 writes a 16-bit value to register addr.
|
||||
func (d *Device8SPI) Write16(addr uint8, value uint16) error {
|
||||
return d.d.Write16SPI(d.bus, addr, value, d.order)
|
||||
}
|
||||
|
||||
// Write32 writes a 32-bit value to register addr.
|
||||
func (d *Device8SPI) Write32(addr uint8, value uint32) error {
|
||||
return d.d.Write32SPI(d.bus, addr, value, d.order)
|
||||
}
|
||||
|
||||
// Device8I2C implements common logic to most 8-bit peripherals with an I2C bus.
|
||||
// All methods expect the target to support conventional register read and write operations
|
||||
// where the first byte sent is the register address being accessed.
|
||||
//
|
||||
// All methods use an internal buffer and perform no dynamic memory allocation.
|
||||
type Device8I2C struct {
|
||||
bus drivers.I2C
|
||||
i2cAddr uint16
|
||||
order binary.ByteOrder
|
||||
d Device8
|
||||
}
|
||||
|
||||
// SetBus sets the I2C bus, device address, and byte order for the Device8I2C.
|
||||
//
|
||||
// As a hint, most I2C devices use big-endian (MSB) byte order.
|
||||
// - Big endian: A value of 0x1234 is transmitted as 0x12 followed by 0x34.
|
||||
// - Little endian: A value of 0x1234 is transmitted as 0x34 followed by 0x12.
|
||||
func (d *Device8I2C) SetBus(bus drivers.I2C, i2cAddr uint16, order binary.ByteOrder) {
|
||||
d.bus = bus
|
||||
d.i2cAddr = i2cAddr
|
||||
d.order = order
|
||||
}
|
||||
|
||||
// Read8 reads a single byte from register addr.
|
||||
func (d *Device8I2C) Read8(addr uint8) (byte, error) {
|
||||
return d.d.Read8I2C(d.bus, d.i2cAddr, addr)
|
||||
}
|
||||
|
||||
// Read16 reads a 16-bit value from register addr.
|
||||
func (d *Device8I2C) Read16(addr uint8) (uint16, error) {
|
||||
return d.d.Read16I2C(d.bus, d.i2cAddr, addr, d.order)
|
||||
}
|
||||
|
||||
// Read32 reads a 32-bit value from register addr.
|
||||
func (d *Device8I2C) Read32(addr uint8) (uint32, error) {
|
||||
return d.d.Read32I2C(d.bus, d.i2cAddr, addr, d.order)
|
||||
}
|
||||
|
||||
// ReadData reads dataLength bytes from register addr.
|
||||
func (d *Device8I2C) ReadData(addr uint8, dataDestination []byte) error {
|
||||
return d.d.ReadDataI2C(d.bus, d.i2cAddr, addr, dataDestination)
|
||||
}
|
||||
|
||||
// Write8 writes a single byte value to register addr.
|
||||
func (d *Device8I2C) Write8(addr, value uint8) error {
|
||||
return d.d.Write8I2C(d.bus, d.i2cAddr, addr, value)
|
||||
}
|
||||
|
||||
// Write16 writes a 16-bit value to register addr.
|
||||
func (d *Device8I2C) Write16(addr uint8, value uint16) error {
|
||||
return d.d.Write16I2C(d.bus, d.i2cAddr, addr, value, d.order)
|
||||
}
|
||||
|
||||
// Write32 writes a 32-bit value to register addr.
|
||||
func (d *Device8I2C) Write32(addr uint8, value uint32) error {
|
||||
return d.d.Write32I2C(d.bus, d.i2cAddr, addr, value, d.order)
|
||||
}
|
||||
+115
-36
@@ -11,37 +11,57 @@ import (
|
||||
// Device wraps an I2C connection to a LIS3DH device.
|
||||
type Device struct {
|
||||
bus drivers.I2C
|
||||
Address uint16
|
||||
address uint16
|
||||
r Range
|
||||
accel [6]byte // stored acceleration data (from the Update call)
|
||||
}
|
||||
|
||||
// Driver configuration, used for the Configure call. All fields are optional.
|
||||
type Config struct {
|
||||
Address uint16
|
||||
}
|
||||
|
||||
// New creates a new LIS3DH connection. The I2C bus must already be configured.
|
||||
//
|
||||
// This function only creates the Device object, it does not touch the device.
|
||||
func New(bus drivers.I2C) Device {
|
||||
return Device{bus: bus, Address: Address0}
|
||||
return Device{bus: bus, address: Address0}
|
||||
}
|
||||
|
||||
// Configure sets up the device for communication
|
||||
func (d *Device) Configure() {
|
||||
func (d *Device) Configure(config Config) error {
|
||||
if config.Address != 0 {
|
||||
d.address = config.Address
|
||||
}
|
||||
|
||||
// enable all axes, normal mode
|
||||
legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL1, []byte{0x07})
|
||||
err := legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL1, []byte{0x07})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 400Hz rate
|
||||
d.SetDataRate(DATARATE_400_HZ)
|
||||
err = d.SetDataRate(DATARATE_400_HZ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// High res & BDU enabled
|
||||
legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL4, []byte{0x88})
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL4, []byte{0x88})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get current range
|
||||
d.r = d.ReadRange()
|
||||
d.r, err = d.ReadRange()
|
||||
return err
|
||||
}
|
||||
|
||||
// Connected returns whether a LIS3DH has been found.
|
||||
// It does a "who am I" request and checks the response.
|
||||
func (d *Device) Connected() bool {
|
||||
data := []byte{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), WHO_AM_I, data)
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.address), WHO_AM_I, data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -49,46 +69,51 @@ func (d *Device) Connected() bool {
|
||||
}
|
||||
|
||||
// SetDataRate sets the speed of data collected by the LIS3DH.
|
||||
func (d *Device) SetDataRate(rate DataRate) {
|
||||
func (d *Device) SetDataRate(rate DataRate) error {
|
||||
ctl1 := []byte{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CTRL1, ctl1)
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.address), REG_CTRL1, ctl1)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return err
|
||||
}
|
||||
// mask off bits
|
||||
ctl1[0] &^= 0xf0
|
||||
ctl1[0] |= (byte(rate) << 4)
|
||||
legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL1, ctl1)
|
||||
return legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL1, ctl1)
|
||||
}
|
||||
|
||||
// SetRange sets the G range for LIS3DH.
|
||||
func (d *Device) SetRange(r Range) {
|
||||
func (d *Device) SetRange(r Range) error {
|
||||
ctl := []byte{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CTRL4, ctl)
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.address), REG_CTRL4, ctl)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return err
|
||||
}
|
||||
// mask off bits
|
||||
ctl[0] &^= 0x30
|
||||
ctl[0] |= (byte(r) << 4)
|
||||
legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL4, ctl)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL4, ctl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// store the new range
|
||||
d.r = r
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadRange returns the current G range for LIS3DH.
|
||||
func (d *Device) ReadRange() (r Range) {
|
||||
func (d *Device) ReadRange() (r Range, err error) {
|
||||
ctl := []byte{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CTRL4, ctl)
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.address), REG_CTRL4, ctl)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return 0, err
|
||||
}
|
||||
// mask off bits
|
||||
r = Range(ctl[0] >> 4)
|
||||
r &= 0x03
|
||||
|
||||
return r
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ReadAcceleration reads the current acceleration from the device and returns
|
||||
@@ -96,28 +121,17 @@ func (d *Device) ReadRange() (r Range) {
|
||||
// and the sensor is not moving the returned value will be around 1000000 or
|
||||
// -1000000.
|
||||
func (d *Device) ReadAcceleration() (int32, int32, int32, error) {
|
||||
x, y, z := d.ReadRawAcceleration()
|
||||
divider := float32(1)
|
||||
switch d.r {
|
||||
case RANGE_16_G:
|
||||
divider = 1365
|
||||
case RANGE_8_G:
|
||||
divider = 4096
|
||||
case RANGE_4_G:
|
||||
divider = 8190
|
||||
case RANGE_2_G:
|
||||
divider = 16380
|
||||
}
|
||||
|
||||
return int32(float32(x) / divider * 1000000), int32(float32(y) / divider * 1000000), int32(float32(z) / divider * 1000000), nil
|
||||
rawX, rawY, rawZ := d.ReadRawAcceleration()
|
||||
x, y, z := normalizeRange(rawX, rawY, rawZ, d.r)
|
||||
return x, y, z, nil
|
||||
}
|
||||
|
||||
// ReadRawAcceleration returns the raw x, y and z axis from the LIS3DH
|
||||
func (d *Device) ReadRawAcceleration() (x int16, y int16, z int16) {
|
||||
legacy.WriteRegister(d.bus, uint8(d.Address), REG_OUT_X_L|0x80, nil)
|
||||
legacy.WriteRegister(d.bus, uint8(d.address), REG_OUT_X_L|0x80, nil)
|
||||
|
||||
data := []byte{0, 0, 0, 0, 0, 0}
|
||||
d.bus.Tx(d.Address, nil, data)
|
||||
d.bus.Tx(d.address, nil, data)
|
||||
|
||||
x = int16((uint16(data[1]) << 8) | uint16(data[0]))
|
||||
y = int16((uint16(data[3]) << 8) | uint16(data[2]))
|
||||
@@ -125,3 +139,68 @@ func (d *Device) ReadRawAcceleration() (x int16, y int16, z int16) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Update the sensor values of the 'which' parameter. Only acceleration is
|
||||
// supported at the moment.
|
||||
func (d *Device) Update(which drivers.Measurement) error {
|
||||
if which&drivers.Acceleration != 0 {
|
||||
// Read raw acceleration values and store them in the driver.
|
||||
err := legacy.WriteRegister(d.bus, uint8(d.address), REG_OUT_X_L|0x80, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.bus.Tx(d.address, nil, d.accel[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Acceleration returns the last read acceleration in µg (micro-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.
|
||||
func (d *Device) Acceleration() (x, y, z int32) {
|
||||
// Extract the raw 16-bit values.
|
||||
rawX := int16((uint16(d.accel[1]) << 8) | uint16(d.accel[0]))
|
||||
rawY := int16((uint16(d.accel[3]) << 8) | uint16(d.accel[2]))
|
||||
rawZ := int16((uint16(d.accel[5]) << 8) | uint16(d.accel[4]))
|
||||
|
||||
// Normalize these values, to be in µg (micro-gravity).
|
||||
return normalizeRange(rawX, rawY, rawZ, d.r)
|
||||
}
|
||||
|
||||
// Convert raw 16-bit values to normalized 32-bit values while avoiding floats
|
||||
// and divisions.
|
||||
func normalizeRange(rawX, rawY, rawZ int16, r Range) (x, y, z int32) {
|
||||
// We're going to convert the 16-bit raw values to values in the range
|
||||
// -1000_000..1000_000. For now we're going to assume a range of 16G, we'll
|
||||
// adjust that range later.
|
||||
// The formula is derived as follows, and carefully selected to avoid
|
||||
// overflow and integer divisions (the division will be optimized to a
|
||||
// bitshift):
|
||||
// x = x * 1000_000 / 2048
|
||||
// x = x * (1000_000/64) / (2048/64)
|
||||
// x = x * 15625 / 32
|
||||
x = int32(rawX) * 15625 / 32
|
||||
y = int32(rawY) * 15625 / 32
|
||||
z = int32(rawZ) * 15625 / 32
|
||||
|
||||
// Now we need to normalize the three values, since we assumed 16G before.
|
||||
shift := uint32(0)
|
||||
switch r {
|
||||
case RANGE_16_G:
|
||||
shift = 0
|
||||
case RANGE_8_G:
|
||||
shift = 1
|
||||
case RANGE_4_G:
|
||||
shift = 2
|
||||
case RANGE_2_G:
|
||||
shift = 3
|
||||
}
|
||||
x >>= shift
|
||||
y >>= shift
|
||||
z >>= shift
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+14
-10
@@ -60,16 +60,20 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
Bandwidth_7_8 = iota // 7.8 kHz
|
||||
Bandwidth_10_4 // 10.4 kHz
|
||||
Bandwidth_15_6 // 15.6 kHz
|
||||
Bandwidth_20_8 // 20.8 kHz
|
||||
Bandwidth_31_25 // 31.25 kHz
|
||||
Bandwidth_41_7 // 41.7 kHz
|
||||
Bandwidth_62_5 // 62.5 kHz
|
||||
Bandwidth_125_0 // 125.0 kHz
|
||||
Bandwidth_250_0 // 250.0 kHz
|
||||
Bandwidth_500_0 // 500.0 kHz
|
||||
Bandwidth_7_8 = iota // 7.8 kHz
|
||||
Bandwidth_10_4 // 10.4 kHz
|
||||
Bandwidth_15_6 // 15.6 kHz
|
||||
Bandwidth_20_8 // 20.8 kHz
|
||||
Bandwidth_31_25 // 31.25 kHz
|
||||
Bandwidth_41_7 // 41.7 kHz
|
||||
Bandwidth_62_5 // 62.5 kHz
|
||||
Bandwidth_125_0 // 125.0 kHz
|
||||
Bandwidth_203_125 // 203.125 kHz
|
||||
Bandwidth_250_0 // 250.0 kHz
|
||||
Bandwidth_406_25 // 406.25 kHz
|
||||
Bandwidth_500_0 // 500.0 kHz
|
||||
Bandwidth_812_5 // 812.5 kHz
|
||||
Bandwidth_1625_0 // 1625 kHz
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -3,8 +3,8 @@ package region
|
||||
import "tinygo.org/x/drivers/lora"
|
||||
|
||||
const (
|
||||
EU868_DEFAULT_PREAMBLE_LEN = 8
|
||||
EU868_DEFAULT_TX_POWER_DBM = 20
|
||||
EU868_DEFAULT_PREAMBLE_LEN = 8 // page 103 RP002-1.0.5
|
||||
EU868_DEFAULT_TX_POWER_DBM = 16 // page 36 RP002-1.0.5, 16 is the max
|
||||
)
|
||||
|
||||
type ChannelEU struct {
|
||||
|
||||
@@ -6,6 +6,9 @@ const (
|
||||
RadioEventTimeout
|
||||
RadioEventWatchdog
|
||||
RadioEventCrcError
|
||||
RadioEventValidHeader
|
||||
RadioEventCadDone
|
||||
RadioEventCadDetected
|
||||
RadioEventUnhandled
|
||||
)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ type Configuration struct {
|
||||
MagDataRate uint8
|
||||
}
|
||||
|
||||
var errNotConnected = errors.New("lsm303agr: failed to communicate with either acel or magnet sensor")
|
||||
var errNotConnected = errors.New("lsm303agr: failed to communicate with either accel or magnet sensor")
|
||||
|
||||
// New creates a new LSM303AGR connection. The I2C bus must already be configured.
|
||||
//
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// Package lsm303dlhc implements a driver for the LSM303dlhc,
|
||||
// a 3 axis accelerometer/magnetic sensor typically available on breakout boards.
|
||||
//
|
||||
// Datasheet: https://www.st.com/resource/en/datasheet/lsm303dlhc.pdf
|
||||
|
||||
package lsm303dlhc // import "tinygo.org/x/drivers/lsm303dlhc"
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
)
|
||||
|
||||
// Device wraps an I2C connection to a LSM303dlhc device.
|
||||
type Device struct {
|
||||
bus drivers.I2C
|
||||
AccelAddress uint8
|
||||
MagAddress uint8
|
||||
AccelPowerMode uint8
|
||||
AccelRange uint8
|
||||
AccelDataRate uint8
|
||||
MagPowerMode uint8
|
||||
MagSystemMode uint8
|
||||
MagDataRate uint8
|
||||
buf [6]uint8
|
||||
}
|
||||
|
||||
// Configuration for LSM303dlhc device.
|
||||
type Configuration struct {
|
||||
AccelPowerMode uint8
|
||||
AccelRange uint8
|
||||
AccelDataRate uint8
|
||||
MagPowerMode uint8
|
||||
MagSystemMode uint8
|
||||
MagDataRate uint8
|
||||
}
|
||||
|
||||
// New creates a new LSM303DLHC connection. The I2C bus must already be configured.
|
||||
// This function only creates the Device object, it does not touch the device.
|
||||
func New(bus drivers.I2C) *Device {
|
||||
return &Device{
|
||||
bus: bus,
|
||||
AccelAddress: ACCEL_ADDRESS,
|
||||
MagAddress: MAG_ADDRESS,
|
||||
}
|
||||
}
|
||||
|
||||
// Configure sets up the LSM303dlhc device for communication.
|
||||
func (d *Device) Configure(cfg Configuration) (err error) {
|
||||
|
||||
if cfg.AccelDataRate != 0 {
|
||||
d.AccelDataRate = cfg.AccelDataRate
|
||||
} else {
|
||||
d.AccelDataRate = ACCEL_DATARATE_100HZ
|
||||
}
|
||||
|
||||
if cfg.AccelPowerMode != 0 {
|
||||
d.AccelPowerMode = cfg.AccelPowerMode
|
||||
} else {
|
||||
d.AccelPowerMode = ACCEL_POWER_NORMAL
|
||||
}
|
||||
|
||||
if cfg.AccelRange != 0 {
|
||||
d.AccelRange = cfg.AccelRange
|
||||
} else {
|
||||
d.AccelRange = ACCEL_RANGE_2G
|
||||
}
|
||||
|
||||
if cfg.MagPowerMode != 0 {
|
||||
d.MagPowerMode = cfg.MagPowerMode
|
||||
} else {
|
||||
d.MagPowerMode = MAG_POWER_NORMAL
|
||||
}
|
||||
|
||||
if cfg.MagDataRate != 0 {
|
||||
d.MagDataRate = cfg.MagDataRate
|
||||
} else {
|
||||
d.MagDataRate = MAG_DATARATE_10HZ
|
||||
}
|
||||
|
||||
if cfg.MagSystemMode != 0 {
|
||||
d.MagSystemMode = cfg.MagSystemMode
|
||||
} else {
|
||||
d.MagSystemMode = MAG_SYSTEM_CONTINUOUS
|
||||
}
|
||||
|
||||
data := d.buf[:1]
|
||||
|
||||
data[0] = byte(d.AccelDataRate<<4 | d.AccelPowerMode | 0x07)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), ACCEL_CTRL_REG1_A, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data[0] = byte(0x80 | d.AccelRange<<4)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), ACCEL_CTRL_REG4_A, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data[0] = byte(0xC0)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), CRA_REG_M, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Temperature compensation is on for magnetic sensor
|
||||
data[0] = byte(0x80 | d.MagPowerMode<<4 | d.MagDataRate<<2 | d.MagSystemMode)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.MagAddress), MAG_MR_REG_M, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadAcceleration reads the current acceleration from the device and returns
|
||||
// it in µg (micro-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.
|
||||
func (d *Device) ReadAcceleration() (x, y, z int32, err error) {
|
||||
data := d.buf[:6]
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), ACCEL_OUT_AUTO_INC, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rangeFactor := int16(0)
|
||||
switch d.AccelRange {
|
||||
case ACCEL_RANGE_2G:
|
||||
rangeFactor = 1
|
||||
case ACCEL_RANGE_4G:
|
||||
rangeFactor = 2
|
||||
case ACCEL_RANGE_8G:
|
||||
rangeFactor = 4
|
||||
case ACCEL_RANGE_16G:
|
||||
rangeFactor = 12 // the readings in 16G are a bit lower
|
||||
}
|
||||
|
||||
x = int32(int32(int16((uint16(data[1])<<8|uint16(data[0])))>>4*rangeFactor) * 1000000 / 1024)
|
||||
y = int32(int32(int16((uint16(data[3])<<8|uint16(data[2])))>>4*rangeFactor) * 1000000 / 1024)
|
||||
z = int32(int32(int16((uint16(data[5])<<8|uint16(data[4])))>>4*rangeFactor) * 1000000 / 1024)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadPitchRoll reads the current pitch and roll angles from the device and
|
||||
// returns it in micro-degrees. When the z axis is pointing straight to Earth
|
||||
// the returned values of pitch and roll would be zero.
|
||||
func (d *Device) ReadPitchRoll() (pitch, roll int32, err error) {
|
||||
|
||||
x, y, z, err := d.ReadAcceleration()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
xf, yf, zf := float64(x), float64(y), float64(z)
|
||||
pitch = int32((math.Round(math.Atan2(yf, math.Sqrt(math.Pow(xf, 2)+math.Pow(zf, 2)))*(180/math.Pi)*100) / 100) * 1000000)
|
||||
roll = int32((math.Round(math.Atan2(xf, math.Sqrt(math.Pow(yf, 2)+math.Pow(zf, 2)))*(180/math.Pi)*100) / 100) * 1000000)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// ReadMagneticField reads the current magnetic field from the device and returns
|
||||
// it in mG (milligauss). 1 mG = 0.1 µT (microtesla).
|
||||
func (d *Device) ReadMagneticField() (x, y, z int32, err error) {
|
||||
|
||||
if d.MagSystemMode == MAG_SYSTEM_SINGLE {
|
||||
cmd := d.buf[:1]
|
||||
cmd[0] = byte(0x80 | d.MagPowerMode<<4 | d.MagDataRate<<2 | d.MagSystemMode)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.MagAddress), MAG_MR_REG_M, cmd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data := d.buf[0:6]
|
||||
legacy.ReadRegister(d.bus, uint8(d.MagAddress), MAG_OUT_AUTO_INC, data)
|
||||
|
||||
x = int32(int16((uint16(data[1])<<8 | uint16(data[0]))))
|
||||
y = int32(int16((uint16(data[3])<<8 | uint16(data[2]))))
|
||||
z = int32(int16((uint16(data[5])<<8 | uint16(data[4]))))
|
||||
return
|
||||
}
|
||||
|
||||
// ReadCompass reads the current compass heading from the device and returns
|
||||
// it in micro-degrees. When the z axis is pointing straight to Earth and
|
||||
// the y axis is pointing to North, the heading would be zero.
|
||||
//
|
||||
// However, the heading may be off due to electronic compasses would be effected
|
||||
// by strong magnetic fields and require constant calibration.
|
||||
func (d *Device) ReadCompass() (h int32, err error) {
|
||||
|
||||
x, y, _, err := d.ReadMagneticField()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
xf, yf := float64(x), float64(y)
|
||||
h = int32(float32((180/math.Pi)*math.Atan2(yf, xf)) * 1000000)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadTemperature returns the temperature in Celsius milli degrees (°C/1000)
|
||||
func (d *Device) ReadTemperature() (t int32, err error) {
|
||||
|
||||
data := d.buf[:2]
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.MagAddress), TEMP_OUT_AUTO_INC, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r := int16((uint16(data[1])<<8 | uint16(data[0]))) >> 4 // temperature offset from 25 °C
|
||||
t = 25000 + int32((float32(r)/8)*1000)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package lsm303dlhc
|
||||
|
||||
const (
|
||||
|
||||
// Constants/addresses used for I2C.
|
||||
ACCEL_ADDRESS = 0x19
|
||||
MAG_ADDRESS = 0x1E
|
||||
|
||||
// i2C 8-bit subaddress (SUB): the 7 LSb represent the actual register address
|
||||
// while the MSB enables address auto increment.
|
||||
// If the MSb of the SUB field is 1, the SUB (register address) is
|
||||
// automatically increased to allow multiple data read/writes.
|
||||
ADDR_AUTO_INC_MASK = 0x80
|
||||
|
||||
// accelerometer registers.
|
||||
ACCEL_CTRL_REG1_A = 0x20
|
||||
ACCEL_CTRL_REG4_A = 0x23
|
||||
ACCEL_OUT_X_L_A = 0x28
|
||||
ACCEL_OUT_X_H_A = 0x29
|
||||
ACCEL_OUT_Y_L_A = 0x2A
|
||||
ACCEL_OUT_Y_H_A = 0x2B
|
||||
ACCEL_OUT_Z_L_A = 0x2C
|
||||
ACCEL_OUT_Z_H_A = 0x2D
|
||||
ACCEL_OUT_AUTO_INC = ACCEL_OUT_X_L_A | ADDR_AUTO_INC_MASK
|
||||
|
||||
// magnetic sensor registers.
|
||||
MAG_MR_REG_M = 0x02
|
||||
MAG_OUT_X_L_M = 0x68
|
||||
MAG_OUT_X_H_M = 0x69
|
||||
MAG_OUT_Y_L_M = 0x6A
|
||||
MAG_OUT_Y_H_M = 0x6B
|
||||
MAG_OUT_Z_L_M = 0x6C
|
||||
MAG_OUT_Z_H_M = 0x6D
|
||||
MAG_OUT_AUTO_INC = MAG_OUT_X_L_M | ADDR_AUTO_INC_MASK
|
||||
|
||||
// temperature sensor registers.
|
||||
CRA_REG_M = 0x80
|
||||
TEMP_OUT_L_M = 0x32
|
||||
TEMP_OUT_H_M = 0x31
|
||||
TEMP_OUT_AUTO_INC = TEMP_OUT_L_M | ADDR_AUTO_INC_MASK
|
||||
|
||||
// accelerometer power mode.
|
||||
ACCEL_POWER_NORMAL = 0x00 // default
|
||||
ACCEL_POWER_LOW = 0x08
|
||||
|
||||
// accelerometer range.
|
||||
ACCEL_RANGE_2G = 0x00 // default
|
||||
ACCEL_RANGE_4G = 0x01
|
||||
ACCEL_RANGE_8G = 0x02
|
||||
ACCEL_RANGE_16G = 0x03
|
||||
|
||||
// accelerometer data rate.
|
||||
ACCEL_DATARATE_1HZ = 0x01
|
||||
ACCEL_DATARATE_10HZ = 0x02
|
||||
ACCEL_DATARATE_25HZ = 0x03
|
||||
ACCEL_DATARATE_50HZ = 0x04
|
||||
ACCEL_DATARATE_100HZ = 0x05 // default
|
||||
ACCEL_DATARATE_200HZ = 0x06
|
||||
ACCEL_DATARATE_400HZ = 0x07
|
||||
ACCEL_DATARATE_1344HZ = 0x09 // 5376Hz in low-power mode
|
||||
|
||||
// magnetic sensor power mode.
|
||||
MAG_POWER_NORMAL = 0x00 // default
|
||||
MAG_POWER_LOW = 0x01
|
||||
|
||||
// magnetic sensor operate mode.
|
||||
MAG_SYSTEM_CONTINUOUS = 0x00 // default
|
||||
MAG_SYSTEM_SINGLE = 0x01
|
||||
|
||||
// magnetic sensor data rate
|
||||
MAG_DATARATE_10HZ = 0x00 // default
|
||||
MAG_DATARATE_20HZ = 0x01
|
||||
MAG_DATARATE_50HZ = 0x02
|
||||
MAG_DATARATE_100HZ = 0x03
|
||||
)
|
||||
+35
-24
@@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
)
|
||||
|
||||
type AccelRange uint8
|
||||
@@ -26,7 +25,7 @@ type Device struct {
|
||||
accelSampleRate AccelSampleRate
|
||||
gyroRange GyroRange
|
||||
gyroSampleRate GyroSampleRate
|
||||
buf [6]uint8
|
||||
buf [7]uint8 // up to 6 bytes for read + 1 byte for the register address
|
||||
}
|
||||
|
||||
// Configuration for LSM6DS3TR device.
|
||||
@@ -84,30 +83,20 @@ func (d *Device) doConfigure(cfg Configuration) (err error) {
|
||||
d.gyroSampleRate = GYRO_SR_104
|
||||
}
|
||||
|
||||
data := d.buf[:1]
|
||||
|
||||
// Configure accelerometer
|
||||
data[0] = uint8(d.accelRange) | uint8(d.accelSampleRate)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL1_XL, data)
|
||||
err = d.writeByte(CTRL1_XL, uint8(d.accelRange)|uint8(d.accelSampleRate))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Set ODR bit
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.Address), CTRL4_C, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data[0] = data[0] &^ BW_SCAL_ODR_ENABLED
|
||||
data[0] |= BW_SCAL_ODR_ENABLED
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL4_C, data)
|
||||
// Enable ODR scaling
|
||||
err = d.setBits(CTRL4_C, BW_SCAL_ODR_ENABLED)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Configure gyroscope
|
||||
data[0] = uint8(d.gyroRange) | uint8(d.gyroSampleRate)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL2_G, data)
|
||||
err = d.writeByte(CTRL2_G, uint8(d.gyroRange)|uint8(d.gyroSampleRate))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -118,8 +107,10 @@ func (d *Device) doConfigure(cfg Configuration) (err error) {
|
||||
// Connected returns whether a LSM6DS3TR has been found.
|
||||
// It does a "who am I" request and checks the response.
|
||||
func (d *Device) Connected() bool {
|
||||
data := d.buf[:1]
|
||||
legacy.ReadRegister(d.bus, uint8(d.Address), WHO_AM_I, data)
|
||||
data, err := d.readBytes(WHO_AM_I, 1)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return data[0] == 0x6A
|
||||
}
|
||||
|
||||
@@ -128,8 +119,7 @@ func (d *Device) Connected() bool {
|
||||
// and the sensor is not moving the returned value will be around 1000000 or
|
||||
// -1000000.
|
||||
func (d *Device) ReadAcceleration() (x, y, z int32, err error) {
|
||||
data := d.buf[:6]
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.Address), OUTX_L_XL, data)
|
||||
data, err := d.readBytes(OUTX_L_XL, 6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -153,8 +143,7 @@ func (d *Device) ReadAcceleration() (x, y, z int32, err error) {
|
||||
// rotation along one axis and while doing so integrate all values over time,
|
||||
// you would get a value close to 360000000.
|
||||
func (d *Device) ReadRotation() (x, y, z int32, err error) {
|
||||
data := d.buf[:6]
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.Address), OUTX_L_G, data)
|
||||
data, err := d.readBytes(OUTX_L_G, 6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -177,8 +166,7 @@ func (d *Device) ReadRotation() (x, y, z int32, err error) {
|
||||
|
||||
// ReadTemperature returns the temperature in celsius milli degrees (°C/1000)
|
||||
func (d *Device) ReadTemperature() (t int32, err error) {
|
||||
data := d.buf[:2]
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.Address), OUT_TEMP_L, data)
|
||||
data, err := d.readBytes(OUT_TEMP_L, 2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -187,3 +175,26 @@ func (d *Device) ReadTemperature() (t int32, err error) {
|
||||
t = 25000 + (int32(int16((int16(data[1])<<8)|int16(data[0])))*125)/32
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) readBytes(reg, size uint8) ([]byte, error) {
|
||||
d.buf[0] = reg
|
||||
err := d.bus.Tx(d.Address, d.buf[0:1], d.buf[1:size+1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.buf[1 : size+1], nil
|
||||
}
|
||||
|
||||
func (d *Device) writeByte(reg, value uint8) error {
|
||||
d.buf[0] = reg
|
||||
d.buf[1] = value
|
||||
return d.bus.Tx(d.Address, d.buf[0:2], nil)
|
||||
}
|
||||
|
||||
func (d *Device) setBits(reg, bits uint8) error {
|
||||
data, err := d.readBytes(reg, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.writeByte(reg, (data[0]&^bits)|bits)
|
||||
}
|
||||
|
||||
+35
-28
@@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
)
|
||||
|
||||
type AccelRange uint8
|
||||
@@ -28,7 +27,7 @@ type Device struct {
|
||||
accelMultiplier int32
|
||||
gyroMultiplier int32
|
||||
magMultiplier int32
|
||||
buf [6]uint8
|
||||
buf [7]uint8 // up to 6 bytes for read + 1 byte for the register address
|
||||
}
|
||||
|
||||
// Configuration for LSM9DS1 device.
|
||||
@@ -61,10 +60,15 @@ func New(bus drivers.I2C) *Device {
|
||||
// Case of boolean false and error nil means I2C is up,
|
||||
// but "who am I" responses have unexpected values.
|
||||
func (d *Device) Connected() bool {
|
||||
data1, data2 := d.buf[:1], d.buf[1:2]
|
||||
legacy.ReadRegister(d.bus, d.AccelAddress, WHO_AM_I, data1)
|
||||
legacy.ReadRegister(d.bus, d.MagAddress, WHO_AM_I_M, data2)
|
||||
return data1[0] == 0x68 && data2[0] == 0x3D
|
||||
data, err := d.readBytes(d.AccelAddress, WHO_AM_I, 1)
|
||||
if err != nil || data[0] != 0x68 {
|
||||
return false
|
||||
}
|
||||
data, err = d.readBytes(d.MagAddress, WHO_AM_I_M, 1)
|
||||
if err != nil || data[0] != 0x3D {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ReadAcceleration reads the current acceleration from the device and returns
|
||||
@@ -72,8 +76,7 @@ func (d *Device) Connected() bool {
|
||||
// and the sensor is not moving the returned value will be around 1000000 or
|
||||
// -1000000.
|
||||
func (d *Device) ReadAcceleration() (x, y, z int32, err error) {
|
||||
data := d.buf[:6]
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), OUT_X_L_XL, data)
|
||||
data, err := d.readBytes(d.AccelAddress, OUT_X_L_XL, 6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -88,8 +91,7 @@ func (d *Device) ReadAcceleration() (x, y, z int32, err error) {
|
||||
// rotation along one axis and while doing so integrate all values over time,
|
||||
// you would get a value close to 360000000.
|
||||
func (d *Device) ReadRotation() (x, y, z int32, err error) {
|
||||
data := d.buf[:6]
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), OUT_X_L_G, data)
|
||||
data, err := d.readBytes(d.AccelAddress, OUT_X_L_G, 6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -102,8 +104,7 @@ func (d *Device) ReadRotation() (x, y, z int32, err error) {
|
||||
// ReadMagneticField reads the current magnetic field from the device and returns
|
||||
// it in nT (nanotesla). 1 G (gauss) = 100_000 nT (nanotesla).
|
||||
func (d *Device) ReadMagneticField() (x, y, z int32, err error) {
|
||||
data := d.buf[:6]
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.MagAddress), OUT_X_L_M, data)
|
||||
data, err := d.readBytes(d.MagAddress, OUT_X_L_M, 6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -115,8 +116,7 @@ func (d *Device) ReadMagneticField() (x, y, z int32, err error) {
|
||||
|
||||
// ReadTemperature returns the temperature in Celsius milli degrees (°C/1000)
|
||||
func (d *Device) ReadTemperature() (t int32, err error) {
|
||||
data := d.buf[:2]
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), OUT_TEMP_L, data)
|
||||
data, err := d.readBytes(d.AccelAddress, OUT_TEMP_L, 2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -167,20 +167,16 @@ func (d *Device) doConfigure(cfg Configuration) (err error) {
|
||||
d.magMultiplier = 58
|
||||
}
|
||||
|
||||
data := d.buf[:1]
|
||||
|
||||
// Configure accelerometer
|
||||
// Sample rate & measurement range
|
||||
data[0] = uint8(cfg.AccelSampleRate)<<5 | uint8(cfg.AccelRange)<<3
|
||||
err = legacy.WriteRegister(d.bus, d.AccelAddress, CTRL_REG6_XL, data)
|
||||
err = d.writeByte(d.AccelAddress, CTRL_REG6_XL, uint8(cfg.AccelSampleRate)<<5|uint8(cfg.AccelRange)<<3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Configure gyroscope
|
||||
// Sample rate & measurement range
|
||||
data[0] = uint8(cfg.GyroSampleRate)<<5 | uint8(cfg.GyroRange)<<3
|
||||
err = legacy.WriteRegister(d.bus, d.AccelAddress, CTRL_REG1_G, data)
|
||||
err = d.writeByte(d.AccelAddress, CTRL_REG1_G, uint8(cfg.GyroSampleRate)<<5|uint8(cfg.GyroRange)<<3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -190,33 +186,44 @@ func (d *Device) doConfigure(cfg Configuration) (err error) {
|
||||
// Temperature compensation enabled
|
||||
// High-performance mode XY axis
|
||||
// Sample rate
|
||||
data[0] = 0b10000000 | 0b01000000 | uint8(cfg.MagSampleRate)<<2
|
||||
err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG1_M, data)
|
||||
err = d.writeByte(d.MagAddress, CTRL_REG1_M, 0b10000000|0b01000000|uint8(cfg.MagSampleRate)<<2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Measurement range
|
||||
data[0] = uint8(cfg.MagRange) << 5
|
||||
err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG2_M, data)
|
||||
err = d.writeByte(d.MagAddress, CTRL_REG2_M, uint8(cfg.MagRange)<<5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Continuous-conversion mode
|
||||
// https://electronics.stackexchange.com/questions/237397/continuous-conversion-vs-single-conversion-mode
|
||||
data[0] = 0b00000000
|
||||
err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG3_M, data)
|
||||
err = d.writeByte(d.MagAddress, CTRL_REG3_M, 0b00000000)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// High-performance mode Z axis
|
||||
data[0] = 0b00001000
|
||||
err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG4_M, data)
|
||||
err = d.writeByte(d.MagAddress, CTRL_REG4_M, 0b00001000)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) readBytes(addr, reg, size uint8) ([]byte, error) {
|
||||
d.buf[0] = reg
|
||||
err := d.bus.Tx(uint16(addr), d.buf[0:1], d.buf[1:size+1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.buf[1 : size+1], nil
|
||||
}
|
||||
|
||||
func (d *Device) writeByte(addr, reg, value uint8) error {
|
||||
d.buf[0] = reg
|
||||
d.buf[1] = value
|
||||
return d.bus.Tx(uint16(addr), d.buf[0:2], nil)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user