mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec6385c15a | |||
| 7267dc1014 | |||
| 84bfe4a928 |
@@ -1,3 +1,2 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
open_collective: tinygo
|
||||
|
||||
@@ -11,12 +11,13 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/tinygo-org/tinygo:latest
|
||||
options: --user root
|
||||
container: ghcr.io/tinygo-org/tinygo-dev:latest
|
||||
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@v6
|
||||
uses: actions/checkout@v3
|
||||
- name: TinyGo version check
|
||||
run: tinygo version
|
||||
- name: Enforce Go Formatted Code
|
||||
@@ -24,6 +25,4 @@ jobs:
|
||||
- name: Run unit tests
|
||||
run: make unit-test
|
||||
- name: Run build and smoke tests
|
||||
run: |
|
||||
go env -w GOFLAGS=-buildvcs=false
|
||||
make smoke-test
|
||||
run: make smoke-test
|
||||
|
||||
@@ -1,93 +1,3 @@
|
||||
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**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright The TinyGo Authors. All rights reserved.
|
||||
Copyright (c) 2018-2025 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,17 +26,3 @@ 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,14 +3,11 @@
|
||||
[](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 140 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 100 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
|
||||
|
||||
+3
-7
@@ -5,10 +5,9 @@ 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 (
|
||||
@@ -38,11 +37,8 @@ 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 pin.Output, delay uint32) *Device {
|
||||
return New(&bbSPI{SCK: sckPin.Set, SDO: sdoPin.Set, Delay: delay, configurePins: func() {
|
||||
legacy.ConfigurePinOut(sckPin)
|
||||
legacy.ConfigurePinOut(sdoPin)
|
||||
}})
|
||||
func NewSoftwareSPI(sckPin, sdoPin machine.Pin, delay uint32) *Device {
|
||||
return New(&bbSPI{SCK: sckPin, SDO: sdoPin, Delay: delay})
|
||||
}
|
||||
|
||||
// WriteColors writes the given RGBA color slice out using the APA102 protocol.
|
||||
|
||||
+6
-12
@@ -1,9 +1,6 @@
|
||||
package apa102
|
||||
|
||||
import (
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
import "machine"
|
||||
|
||||
// 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.
|
||||
@@ -11,18 +8,15 @@ import (
|
||||
// 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 pin.OutputFunc
|
||||
SDO pin.OutputFunc
|
||||
Delay uint32
|
||||
configurePins func()
|
||||
SCK machine.Pin
|
||||
SDO machine.Pin
|
||||
Delay uint32
|
||||
}
|
||||
|
||||
// Configure sets up the SCK and SDO pins as outputs and sets them low
|
||||
func (s *bbSPI) Configure() {
|
||||
if s.configurePins == nil {
|
||||
panic(legacy.ErrConfigBeforeInstantiated)
|
||||
}
|
||||
s.configurePins()
|
||||
s.SCK.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
s.SDO.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
s.SCK.Low()
|
||||
s.SDO.Low()
|
||||
if s.Delay == 0 {
|
||||
|
||||
+24
-31
@@ -1,36 +1,31 @@
|
||||
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 pin.OutputFunc
|
||||
CSB machine.Pin
|
||||
|
||||
buf [7]byte
|
||||
|
||||
// SPI bus (requires chip select to be usable).
|
||||
bus drivers.SPI
|
||||
configurePins func()
|
||||
Bus drivers.SPI
|
||||
}
|
||||
|
||||
// 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 pin.Output, spi drivers.SPI) *DeviceSPI {
|
||||
func NewSPI(csb machine.Pin, spi drivers.SPI) *DeviceSPI {
|
||||
return &DeviceSPI{
|
||||
csb: csb.Set, // chip select
|
||||
bus: spi,
|
||||
configurePins: func() {
|
||||
legacy.ConfigurePinOut(csb)
|
||||
},
|
||||
CSB: csb, // chip select
|
||||
Bus: spi,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +33,9 @@ func NewSPI(csb pin.Output, 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 {
|
||||
if d.configurePins == nil {
|
||||
return legacy.ErrConfigBeforeInstantiated
|
||||
}
|
||||
d.configurePins()
|
||||
d.csb.High()
|
||||
d.CSB.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
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
|
||||
@@ -93,9 +86,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
|
||||
}
|
||||
@@ -130,9 +123,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
|
||||
}
|
||||
@@ -160,9 +153,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
|
||||
}
|
||||
@@ -208,9 +201,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]
|
||||
}
|
||||
|
||||
@@ -224,7 +217,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()
|
||||
}
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
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
|
||||
)
|
||||
@@ -1,316 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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
@@ -1,387 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// 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
@@ -1,572 +0,0 @@
|
||||
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 (
|
||||
"time"
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Device wraps a GPIO connection to a buzzer.
|
||||
type Device struct {
|
||||
pin pin.OutputFunc
|
||||
pin machine.Pin
|
||||
High bool
|
||||
BPM float64
|
||||
}
|
||||
|
||||
// New returns a new buzzer driver given which pin to use
|
||||
func New(pin pin.Output) Device {
|
||||
func New(pin machine.Pin) Device {
|
||||
return Device{
|
||||
pin: pin.Set,
|
||||
pin: pin,
|
||||
High: false,
|
||||
BPM: 96.0,
|
||||
}
|
||||
@@ -25,14 +25,14 @@ func New(pin pin.Output) Device {
|
||||
|
||||
// On sets the buzzer to a high state.
|
||||
func (l *Device) On() (err error) {
|
||||
l.pin.High()
|
||||
l.pin.Set(true)
|
||||
l.High = true
|
||||
return
|
||||
}
|
||||
|
||||
// Off sets the buzzer to a low state.
|
||||
func (l *Device) Off() (err error) {
|
||||
l.pin.Low()
|
||||
l.pin.Set(false)
|
||||
l.High = false
|
||||
return
|
||||
}
|
||||
|
||||
+36
-288
@@ -5,12 +5,10 @@
|
||||
package ds3231 // import "tinygo.org/x/drivers/ds3231"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/regmap"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
)
|
||||
|
||||
type Mode uint8
|
||||
@@ -19,7 +17,6 @@ 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
|
||||
@@ -27,50 +24,54 @@ type Device struct {
|
||||
//
|
||||
// This function only creates the Device object, it does not touch the device.
|
||||
func New(bus drivers.I2C) Device {
|
||||
d := Device{
|
||||
return 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 {
|
||||
status, err := d.d.Read8(REG_STATUS)
|
||||
data := []byte{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_STATUS, data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (status & (1 << OSF)) == 0x00
|
||||
return (data[0] & (1 << OSF)) == 0x00
|
||||
}
|
||||
|
||||
// IsRunning returns if the oscillator is running
|
||||
func (d *Device) IsRunning() bool {
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
data := []uint8{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CONTROL, data)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (control & (1 << EOSC)) == 0x00
|
||||
return (data[0] & (1 << EOSC)) == 0x00
|
||||
}
|
||||
|
||||
// SetRunning starts the internal oscillator
|
||||
func (d *Device) SetRunning(isRunning bool) error {
|
||||
control, err := d.d.Read8(REG_CONTROL)
|
||||
data := []uint8{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CONTROL, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isRunning {
|
||||
control &^= uint8(1 << EOSC)
|
||||
data[0] &^= uint8(1 << EOSC)
|
||||
} else {
|
||||
control |= 1 << EOSC
|
||||
data[0] |= 1 << EOSC
|
||||
}
|
||||
return d.d.Write8(REG_CONTROL, control)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_CONTROL, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTime sets the date and time in the DS3231. The DS3231 hardware supports
|
||||
@@ -85,16 +86,18 @@ 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 {
|
||||
status, err := d.d.Read8(REG_STATUS)
|
||||
data := []byte{0}
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_STATUS, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status &^= 1 << OSF
|
||||
if err = d.d.Write8(REG_STATUS, status); err != nil {
|
||||
data[0] &^= 1 << OSF
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_STATUS, data)
|
||||
if 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()))
|
||||
@@ -115,16 +118,21 @@ func (d *Device) SetTime(dt time.Time) error {
|
||||
data[5] = uint8ToBCD(uint8(dt.Month()) | centuryFlag)
|
||||
data[6] = uint8ToBCD(year)
|
||||
|
||||
return d.bus.Tx(d.Address, append([]byte{REG_TIMEDATE}, data...), nil)
|
||||
err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_TIMEDATE, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadTime returns the date and time
|
||||
func (d *Device) ReadTime() (dt time.Time, err error) {
|
||||
data := make([]uint8, 7)
|
||||
if err = d.d.ReadData(REG_TIMEDATE, data); err != nil {
|
||||
err = legacy.ReadRegister(d.bus, uint8(d.Address), REG_TIMEDATE, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
second := bcdToInt(data[0] & 0x7F)
|
||||
minute := bcdToInt(data[1])
|
||||
hour := hoursBCDToInt(data[2])
|
||||
@@ -142,264 +150,12 @@ func (d *Device) ReadTime() (dt time.Time, err error) {
|
||||
|
||||
// ReadTemperature returns the temperature in millicelsius (mC)
|
||||
func (d *Device) ReadTemperature() (int32, error) {
|
||||
temp, err := d.d.Read16(REG_TEMP)
|
||||
data := make([]uint8, 2)
|
||||
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_TEMP, data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
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
|
||||
return milliCelsius(data[0], data[1]), nil
|
||||
}
|
||||
|
||||
// milliCelsius converts the raw temperature bytes (msb and lsb) from the DS3231
|
||||
@@ -416,8 +172,8 @@ func (d *Device) isAlarmFired(alarm_num uint8) bool {
|
||||
// 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(tempBytes uint16) int32 {
|
||||
t256 := int16(uint16(tempBytes>>8)<<8 | uint16(tempBytes&0xFF))
|
||||
func milliCelsius(msb uint8, lsb uint8) int32 {
|
||||
t256 := int16(uint16(msb)<<8 | uint16(lsb))
|
||||
t1000 := int32(t256) / 64 * 250
|
||||
return t1000
|
||||
}
|
||||
@@ -444,11 +200,3 @@ 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)
|
||||
t1000 := milliCelsius(0, 0)
|
||||
if t1000 != 0 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0b0000000001000000)
|
||||
t1000 = milliCelsius(0, 0b01000000)
|
||||
if t1000 != 250 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0b0000000010000000)
|
||||
t1000 = milliCelsius(0, 0b10000000)
|
||||
if t1000 != 500 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0b0000000011000000)
|
||||
t1000 = milliCelsius(0, 0b11000000)
|
||||
if t1000 != 750 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0b0000000100000000)
|
||||
t1000 = milliCelsius(1, 0b00000000)
|
||||
if t1000 != 1000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0b0000001000000000)
|
||||
t1000 = milliCelsius(2, 0b00000000)
|
||||
if t1000 != 2000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
// highest temperature is 127.750C
|
||||
t1000 = milliCelsius(0b0111111111000000)
|
||||
t1000 = milliCelsius(0x7f, 0b11000000)
|
||||
if t1000 != 127750 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNegativeMilliCelsius(t *testing.T) {
|
||||
t1000 := milliCelsius(0b1111111111000000)
|
||||
t1000 := milliCelsius(0xff, 0b11000000)
|
||||
if t1000 != -250 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0b1111111110000000)
|
||||
t1000 = milliCelsius(0xff, 0b10000000)
|
||||
if t1000 != -500 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0b1111111101000000)
|
||||
t1000 = milliCelsius(0xff, 0b01000000)
|
||||
if t1000 != -750 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0b1111111100000000)
|
||||
t1000 = milliCelsius(0xff, 0b00000000)
|
||||
if t1000 != -1000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
t1000 = milliCelsius(0b1111111000000000)
|
||||
t1000 = milliCelsius(0xfe, 0b00000000)
|
||||
if t1000 != -2000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
// lowest temperature is -128.000C
|
||||
t1000 = milliCelsius(0b1000000000000000)
|
||||
t1000 = milliCelsius(0x80, 0b00000000)
|
||||
if t1000 != -128000 {
|
||||
t.Fatal(t1000)
|
||||
}
|
||||
|
||||
@@ -46,52 +46,3 @@ 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,66 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// 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,9 +3,10 @@ package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
|
||||
"tinygo.org/x/drivers/ds3231"
|
||||
)
|
||||
|
||||
@@ -25,19 +26,19 @@ func main() {
|
||||
if !running {
|
||||
err := rtc.SetRunning(true)
|
||||
if err != nil {
|
||||
println("Error configuring RTC")
|
||||
fmt.Println("Error configuring RTC")
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
dt, err := rtc.ReadTime()
|
||||
if err != nil {
|
||||
println("Error reading date:", err)
|
||||
fmt.Println("Error reading date:", err)
|
||||
} else {
|
||||
println(dt.Format(time.DateTime))
|
||||
fmt.Printf("Date: %d/%s/%02d %02d:%02d:%02d \r\n", dt.Year(), dt.Month(), dt.Day(), dt.Hour(), dt.Minute(), dt.Second())
|
||||
}
|
||||
temp, _ := rtc.ReadTemperature()
|
||||
println("Temperature:", strconv.FormatFloat(float64(temp)/1000, 'f', -1, 32), "°C")
|
||||
fmt.Printf("Temperature: %.2f °C \r\n", float32(temp)/1000)
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
println("GPS UART Example")
|
||||
machine.UART1.Configure(machine.UARTConfig{BaudRate: 9600})
|
||||
ublox := gps.NewUART(machine.UART1)
|
||||
parser := gps.NewParser()
|
||||
@@ -15,24 +16,14 @@ func main() {
|
||||
for {
|
||||
s, err := ublox.NextSentence()
|
||||
if err != nil {
|
||||
switch err {
|
||||
case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength:
|
||||
continue
|
||||
default:
|
||||
println("sentence error:", err)
|
||||
continue
|
||||
}
|
||||
println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
fix, err = parser.Parse(s)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength:
|
||||
continue
|
||||
default:
|
||||
println("parse error:", err)
|
||||
continue
|
||||
}
|
||||
println(err)
|
||||
continue
|
||||
}
|
||||
if fix.Valid {
|
||||
print(fix.Time.Format("15:04:05"))
|
||||
@@ -52,10 +43,7 @@ func main() {
|
||||
}
|
||||
println()
|
||||
} else {
|
||||
if fix.Type == gps.GSV {
|
||||
// GSV sentence provides satellite count even if no fix yet
|
||||
println(fix.Satellites, "satellites visible")
|
||||
}
|
||||
println("No fix")
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func main() {
|
||||
SDI: machine.SPI0_SDI_PIN,
|
||||
Mode: 0})
|
||||
can := mcp2515.New(spi, csPin)
|
||||
can.Configure(mcp2515.Configuration{})
|
||||
can.Configure()
|
||||
err := can.Begin(mcp2515.CAN500kBps, mcp2515.Clock8MHz)
|
||||
if err != nil {
|
||||
failMessage(err.Error())
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
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,7 +5,6 @@ import (
|
||||
"machine"
|
||||
"math/rand"
|
||||
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
"tinygo.org/x/drivers/ssd1289"
|
||||
)
|
||||
|
||||
@@ -17,7 +16,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]pin.Output{
|
||||
bus := ssd1289.NewPinBus([16]machine.Pin{
|
||||
machine.GP4, //DB0
|
||||
machine.GP5, //DB1
|
||||
machine.GP6, //DB2
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
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 || arduino_uno
|
||||
//go:build arduino
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !digispark && !arduino && !arduino_uno
|
||||
//go:build !digispark && !arduino
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// 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,16 +1,17 @@
|
||||
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,8 +17,6 @@ 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=
|
||||
|
||||
+34
-11
@@ -11,18 +11,37 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
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")
|
||||
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")
|
||||
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 = '$'
|
||||
@@ -31,18 +50,19 @@ 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{},
|
||||
}
|
||||
@@ -59,6 +79,7 @@ func NewI2CWithAddress(bus drivers.I2C, i2cAddress uint16) Device {
|
||||
return Device{
|
||||
bus: bus,
|
||||
address: i2cAddress,
|
||||
buffer: make([]byte, bufferSize),
|
||||
bufIdx: bufferSize,
|
||||
sentence: strings.Builder{},
|
||||
}
|
||||
@@ -151,15 +172,17 @@ 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:] {
|
||||
return ErrInvalidNMEASentence
|
||||
if checksum != sentence[len(sentence)-2:len(sentence)] {
|
||||
return newGPSError(errInvalidNMEAChecksum, sentence,
|
||||
"expected "+sentence[len(sentence)-2:len(sentence)]+
|
||||
" got "+checksum)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+3
-39
@@ -6,28 +6,12 @@ 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
|
||||
|
||||
@@ -62,30 +46,13 @@ 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, ",")
|
||||
@@ -93,7 +60,6 @@ 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])
|
||||
@@ -109,7 +75,6 @@ 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])
|
||||
@@ -124,7 +89,6 @@ 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])
|
||||
@@ -140,7 +104,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
return fix, nil
|
||||
}
|
||||
|
||||
return fix, ErrUnknownNMEASentence
|
||||
return fix, newGPSError(errUnknownNMEASentence, sentence, typ)
|
||||
}
|
||||
|
||||
// findTime returns the time from an NMEA sentence:
|
||||
|
||||
+2
-18
@@ -8,29 +8,13 @@ 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"
|
||||
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)
|
||||
_, err := p.Parse(val)
|
||||
c.Assert(err.Error(), qt.Contains, "unsupported NMEA sentence type")
|
||||
}
|
||||
|
||||
func TestParseGGA(t *testing.T) {
|
||||
|
||||
+39
-240
@@ -1,254 +1,53 @@
|
||||
package gps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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{},
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 SetCfgGNSS(d Device) (err error) {
|
||||
err = sendCommand(d, cfg_gnss_cmd[:])
|
||||
return err
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
func sendCommand(d Device, command []byte) (err error) {
|
||||
d.WriteBytes(command)
|
||||
start := time.Now()
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return errors.New("no ACK to GPS command")
|
||||
}
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
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
@@ -1,220 +0,0 @@
|
||||
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
@@ -1,189 +0,0 @@
|
||||
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.isBusy(command == DISPLAY_CLEAR || command == CURSOR_HOME) {
|
||||
for d.busy(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.isBusy(false) {
|
||||
for d.busy(false) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,9 +231,9 @@ func (d *Device) CreateCharacter(cgramAddr uint8, data []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// isBusy returns true when hd447890 is isBusy
|
||||
// busy returns true when hd447890 is busy
|
||||
// or after the timeout specified
|
||||
func (d *Device) isBusy(longDelay bool) bool {
|
||||
func (d *Device) busy(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) isBusy(longDelay bool) bool {
|
||||
|
||||
// Busy returns true when hd447890 is busy
|
||||
func (d *Device) Busy() bool {
|
||||
return d.isBusy(false)
|
||||
return d.busy(false)
|
||||
}
|
||||
|
||||
// Size returns the current size of the display.
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// Package regmap provides transaction-based interfaces for reading and writing
|
||||
// to device registers over I2C and SPI buses with pre-allocated buffers.
|
||||
package regmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
var (
|
||||
// errNotInTx indicates an operation was attempted outside of an active transaction.
|
||||
errNotInTx = errors.New("device not in Tx")
|
||||
|
||||
// errInTx indicates a transaction was started while another is still active.
|
||||
errInTx = errors.New("device already in Tx")
|
||||
|
||||
// errShortWriteBuffer indicates the write buffer is too small for the requested operation.
|
||||
errShortWriteBuffer = errors.New("device write buffer too short")
|
||||
|
||||
// errShortReadBuffer indicates the read buffer is too small for the requested operation.
|
||||
errShortReadBuffer = errors.New("device read buffer too short")
|
||||
)
|
||||
|
||||
// Device8Txer wraps a Device8 to provide buffered transaction support for
|
||||
// I2C and SPI operations. It maintains pre-allocated buffers to avoid heap
|
||||
// allocations during register access operations.
|
||||
//
|
||||
// Users must call SetBuffers to configure the write and read buffers before
|
||||
// initiating transactions.
|
||||
type Device8Txer struct {
|
||||
Device8
|
||||
writeBuf []byte // Pre-allocated buffer for write operations
|
||||
readBuf []byte // Pre-allocated buffer for read operations
|
||||
inTx bool // Tracks whether a transaction is currently active
|
||||
}
|
||||
|
||||
// SetTxBuffers configures the write and read buffers for this device.
|
||||
// These buffers are reused across transactions to avoid heap allocations.
|
||||
//
|
||||
// The writebuf should be large enough to hold the register address plus
|
||||
// all data bytes to be written in a single transaction.
|
||||
func (d *Device8Txer) SetTxBuffers(writebuf, readbuf []byte) {
|
||||
d.readBuf = readbuf
|
||||
d.writeBuf = writebuf
|
||||
}
|
||||
|
||||
// Tx8 represents an active transaction for an 8-bit register device.
|
||||
// It tracks the write buffer and current offset as data is added to the transaction.
|
||||
//
|
||||
// Use AddWriteByte or AddWriteData to add data to the transaction, then call
|
||||
// DoTxI2C or DoTxSPI to execute the transaction over the bus.
|
||||
type Tx8 struct {
|
||||
dw *Device8Txer // Reference to the parent device
|
||||
off int // Current offset in the write buffer
|
||||
}
|
||||
|
||||
// Tx initiates a new transaction for writing to the specified register address.
|
||||
//
|
||||
// Parameters:
|
||||
// - writeAddr: The 8-bit register address to write to
|
||||
//
|
||||
// Returns a Tx8 handle that can be used to add data and execute the transaction.
|
||||
//
|
||||
// Returns an error if:
|
||||
// - A transaction is already active (errInTx)
|
||||
// - The write buffer is too short (errShortWriteBuffer)
|
||||
func (dw *Device8Txer) Tx(writeAddr uint8) (Tx8, error) {
|
||||
if dw.inTx {
|
||||
return Tx8{}, errInTx
|
||||
} else if len(dw.writeBuf) < 1 {
|
||||
return Tx8{}, errShortWriteBuffer
|
||||
}
|
||||
dw.writeBuf[0] = writeAddr
|
||||
return Tx8{dw: dw, off: 1}, nil
|
||||
}
|
||||
|
||||
// AddWriteData appends multiple bytes to the current transaction's write buffer.
|
||||
//
|
||||
// Parameters:
|
||||
// - buf: Variable number of bytes to add to the transaction
|
||||
//
|
||||
// Returns an error if:
|
||||
// - No transaction is active (errNotInTx)
|
||||
// - The write buffer doesn't have enough space (errShortWriteBuffer)
|
||||
func (tx *Tx8) AddWriteData(buf ...byte) error {
|
||||
if !tx.dw.inTx {
|
||||
return errNotInTx
|
||||
}
|
||||
avail := tx.dw.writeBuf[tx.off:]
|
||||
if len(avail) < len(buf) {
|
||||
return errShortWriteBuffer
|
||||
}
|
||||
n := copy(avail, buf)
|
||||
tx.off += n
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddWriteByte appends a single byte to the current transaction's write buffer.
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The byte to add to the transaction
|
||||
//
|
||||
// Returns an error if:
|
||||
// - No transaction is active (errNotInTx)
|
||||
// - The write buffer doesn't have enough space (errShortWriteBuffer)
|
||||
func (tx *Tx8) AddWriteByte(b byte) error {
|
||||
if !tx.dw.inTx {
|
||||
return errNotInTx
|
||||
}
|
||||
avail := tx.dw.writeBuf[tx.off:]
|
||||
if len(avail) < 1 {
|
||||
return errShortWriteBuffer
|
||||
}
|
||||
avail[0] = b
|
||||
tx.off++
|
||||
return nil
|
||||
}
|
||||
|
||||
// DoTxI2C executes the transaction over an I2C bus.
|
||||
//
|
||||
// This performs a combined write-read I2C transaction, first sending the
|
||||
// register address and any data added to the transaction, then reading
|
||||
// the specified number of bytes from the device.
|
||||
//
|
||||
// Parameters:
|
||||
// - bus: The I2C bus to communicate over
|
||||
// - deviceAddr: The I2C address of the target device
|
||||
// - readLength: Number of bytes to read from the device
|
||||
//
|
||||
// Returns the read data as a slice of the internal read buffer, valid until
|
||||
// the next transaction. The transaction is automatically freed after execution.
|
||||
//
|
||||
// Returns an error if:
|
||||
// - No transaction is active (errNotInTx)
|
||||
// - The read buffer is too short (errShortReadBuffer)
|
||||
// - The I2C transaction fails
|
||||
func (tx *Tx8) DoTxI2C(bus drivers.I2C, deviceAddr uint16, readLength int) ([]byte, error) {
|
||||
if tx.off == 0 || !tx.dw.inTx {
|
||||
return nil, errNotInTx
|
||||
}
|
||||
defer tx.freeTx()
|
||||
if len(tx.dw.readBuf) < readLength {
|
||||
return nil, errShortReadBuffer
|
||||
}
|
||||
rbuf := tx.dw.readBuf[:readLength]
|
||||
err := bus.Tx(deviceAddr, tx.dw.writeBuf[:tx.off], rbuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rbuf, err
|
||||
}
|
||||
|
||||
// DoTxSPI executes the transaction over an SPI bus.
|
||||
//
|
||||
// This performs a full-duplex SPI transaction, simultaneously writing the
|
||||
// register address and data while reading the same number of bytes from the device.
|
||||
//
|
||||
// If no read buffer was configured (readBuf is nil), this performs a write-only
|
||||
// transaction and returns nil without error.
|
||||
//
|
||||
// Parameters:
|
||||
// - bus: The SPI bus to communicate over
|
||||
//
|
||||
// Returns the read data as a slice of the internal read buffer (same length as
|
||||
// the write data), valid until the next transaction. The transaction is
|
||||
// automatically freed after execution.
|
||||
//
|
||||
// Returns an error if:
|
||||
// - No transaction is active (errNotInTx)
|
||||
// - The read buffer is too short (errShortReadBuffer)
|
||||
// - The SPI transaction fails
|
||||
func (tx *Tx8) DoTxSPI(bus drivers.SPI) (readBuf []byte, err error) {
|
||||
if tx.off == 0 || !tx.dw.inTx {
|
||||
return nil, errNotInTx
|
||||
}
|
||||
defer tx.freeTx()
|
||||
if tx.dw.readBuf == nil {
|
||||
err = bus.Tx(tx.dw.writeBuf[:tx.off], nil) // Special case, only use write buffer functionality.
|
||||
return nil, err
|
||||
} else if len(readBuf) < tx.off {
|
||||
return nil, errShortReadBuffer
|
||||
}
|
||||
rbuf := tx.dw.readBuf[:tx.off]
|
||||
err = bus.Tx(tx.dw.writeBuf[:tx.off], rbuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rbuf, err
|
||||
}
|
||||
|
||||
// freeTx marks the transaction as complete, allowing a new transaction to be started.
|
||||
// This is called internally by DoTxI2C and DoTxSPI after the transaction completes.
|
||||
func (tx *Tx8) freeTx() {
|
||||
tx.dw.inTx = false
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package regmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
func ExampleDevice8Txer() {
|
||||
// Initialization.
|
||||
var dtx Device8Txer
|
||||
dtx.SetTxBuffers(make([]byte, 256), make([]byte, 256))
|
||||
|
||||
// Usage.
|
||||
const (
|
||||
defaultAddr = 65
|
||||
REG_WRITE = 0x1f
|
||||
IOCTL_CALL = 0xc0
|
||||
)
|
||||
tx, err := dtx.Tx(REG_WRITE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = tx.AddWriteData(IOCTL_CALL, 0x80, 0x80)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var bus drivers.I2C
|
||||
readData, err := tx.DoTxI2C(bus, defaultAddr, 20)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(readData)
|
||||
}
|
||||
+10
-14
@@ -60,20 +60,16 @@ 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_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
|
||||
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
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,9 +6,6 @@ const (
|
||||
RadioEventTimeout
|
||||
RadioEventWatchdog
|
||||
RadioEventCrcError
|
||||
RadioEventValidHeader
|
||||
RadioEventCadDone
|
||||
RadioEventCadDetected
|
||||
RadioEventUnhandled
|
||||
)
|
||||
|
||||
|
||||
+4
-4
@@ -3,9 +3,9 @@ package max6675
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// ErrThermocoupleOpen is returned when the thermocouple input is open.
|
||||
@@ -14,16 +14,16 @@ var ErrThermocoupleOpen = errors.New("thermocouple input open")
|
||||
|
||||
type Device struct {
|
||||
bus drivers.SPI
|
||||
cs pin.OutputFunc
|
||||
cs machine.Pin
|
||||
}
|
||||
|
||||
// Create a new Device to read from a MAX6675 thermocouple.
|
||||
// Pins must be configured before use. Frequency for SPI
|
||||
// should be 4.3MHz maximum.
|
||||
func NewDevice(bus drivers.SPI, cs pin.Output) *Device {
|
||||
func NewDevice(bus drivers.SPI, cs machine.Pin) *Device {
|
||||
return &Device{
|
||||
bus: bus,
|
||||
cs: cs.Set,
|
||||
cs: cs,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-14
@@ -3,36 +3,31 @@
|
||||
package max72xx
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
bus drivers.SPI
|
||||
cs pin.OutputFunc
|
||||
configurePins func()
|
||||
bus drivers.SPI
|
||||
cs machine.Pin
|
||||
}
|
||||
|
||||
// NewDriver creates a new max7219 connection. The SPI wire must already be configured
|
||||
// The SPI frequency must not be higher than 10MHz.
|
||||
// parameter cs: the datasheet also refers to this pin as "load" pin.
|
||||
func NewDevice(bus drivers.SPI, cs pin.Output) *Device {
|
||||
func NewDevice(bus drivers.SPI, cs machine.Pin) *Device {
|
||||
return &Device{
|
||||
bus: bus,
|
||||
cs: cs.Set,
|
||||
configurePins: func() {
|
||||
legacy.ConfigurePinOut(cs)
|
||||
},
|
||||
cs: cs,
|
||||
}
|
||||
}
|
||||
|
||||
// Configure setups the pins.
|
||||
func (driver *Device) Configure() {
|
||||
if driver.configurePins == nil {
|
||||
panic(legacy.ErrConfigBeforeInstantiated)
|
||||
}
|
||||
driver.configurePins()
|
||||
outPutConfig := machine.PinConfig{Mode: machine.PinOutput}
|
||||
|
||||
driver.cs.Configure(outPutConfig)
|
||||
}
|
||||
|
||||
// SetScanLimit sets the scan limit. Maximum is 8.
|
||||
|
||||
+49
-61
@@ -6,38 +6,20 @@
|
||||
package mcp2515 // import "tinygo.org/x/drivers/mcp2515"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNothingIsReceived = errors.New("readMsg: nothing is received")
|
||||
ErrRequestNewModeMaxTimeEx = errors.New("requestNewMode max time expired")
|
||||
ErrLengthIsLongerThanCapacity = errors.New("length is longer than capacity")
|
||||
ErrTxTimeout = errors.New("Tx: Tx timeout")
|
||||
ErrInvalidDirection = errors.New("invalid direction")
|
||||
ErrInvalidParameter = errors.New("invalid parameter")
|
||||
ErrCannotExpandBuffer = errors.New("cannot expand buffer (to avoid memory allocation)")
|
||||
)
|
||||
|
||||
// Device wraps MCP2515 SPI CAN Module.
|
||||
type Device struct {
|
||||
spi SPI
|
||||
cs pin.OutputFunc
|
||||
msg *CANMsg
|
||||
extended bool
|
||||
mcpMode byte
|
||||
configurePins func()
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
Extended bool
|
||||
spi SPI
|
||||
cs machine.Pin
|
||||
msg *CANMsg
|
||||
mcpMode byte
|
||||
}
|
||||
|
||||
// CANMsg stores CAN message fields.
|
||||
@@ -54,30 +36,23 @@ const (
|
||||
)
|
||||
|
||||
// New returns a new MCP2515 driver. Pass in a fully configured SPI bus.
|
||||
func New(b drivers.SPI, csPin pin.Output) *Device {
|
||||
func New(b drivers.SPI, csPin machine.Pin) *Device {
|
||||
d := &Device{
|
||||
spi: SPI{
|
||||
bus: b,
|
||||
tx: make([]byte, 0, bufferSize),
|
||||
rx: make([]byte, 0, bufferSize),
|
||||
},
|
||||
cs: csPin.Set,
|
||||
cs: csPin,
|
||||
msg: &CANMsg{},
|
||||
configurePins: func() {
|
||||
legacy.ConfigurePinOut(csPin)
|
||||
},
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Configure sets up the device for communication.
|
||||
func (d *Device) Configure(cfg Configuration) {
|
||||
if d.configurePins == nil {
|
||||
panic(legacy.ErrConfigBeforeInstantiated)
|
||||
}
|
||||
d.extended = cfg.Extended
|
||||
d.configurePins()
|
||||
func (d *Device) Configure() {
|
||||
d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
}
|
||||
|
||||
const beginTimeoutValue int = 10
|
||||
@@ -134,13 +109,9 @@ func (d *Device) Tx(canid uint32, dlc uint8, data []byte) error {
|
||||
timeoutCount++
|
||||
}
|
||||
if timeoutCount == timeoutvalue {
|
||||
return ErrTxTimeout
|
||||
return fmt.Errorf("Tx: Tx timeout")
|
||||
}
|
||||
ext := byte(0)
|
||||
if d.extended {
|
||||
ext = 1
|
||||
}
|
||||
err = d.writeCANMsg(bufNum, canid, ext, 0, dlc, data)
|
||||
err = d.writeCANMsg(bufNum, canid, 0, 0, dlc, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -410,7 +381,7 @@ func (d *Device) configRate(speed, clock byte) error {
|
||||
set = false
|
||||
}
|
||||
if !set {
|
||||
return ErrInvalidParameter
|
||||
return errors.New("invalid parameter")
|
||||
}
|
||||
if err := d.setRegister(mcpCNF1, cfg1); err != nil {
|
||||
return err
|
||||
@@ -470,7 +441,7 @@ func (d *Device) readMsg() error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return ErrNothingIsReceived
|
||||
return fmt.Errorf("readMsg: nothing is received")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -577,22 +548,39 @@ func (d *Device) writeCANMsg(bufNum uint8, canid uint32, ext, rtrBit, dlc uint8,
|
||||
}
|
||||
|
||||
func (s *SPI) setTxBufData(canid uint32, ext, rtrBit, dlc uint8, data []byte) error {
|
||||
var id [4]byte
|
||||
canid = canid & 0x0FFFF
|
||||
if ext == 1 {
|
||||
canid = canid & extidBottom29Mask
|
||||
extended_id := canid
|
||||
high_11 := extended_id & extidTop11WriteMask
|
||||
low_18 := extended_id & extidBottom18Mask
|
||||
high_11 <<= 3
|
||||
extended_id_shifted := high_11 | low_18
|
||||
canid = extended_id_shifted | extidFlagMask
|
||||
// TODO: add Extended ID
|
||||
err := s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
canid = canid & stdidBottom11Mask
|
||||
canid <<= 16 + 5
|
||||
}
|
||||
binary.BigEndian.PutUint32(id[:], canid)
|
||||
for _, b := range id {
|
||||
err := s.setTxData(b)
|
||||
err := s.setTxData(byte(canid >> 3))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(byte((canid & 0x07) << 5))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -789,7 +777,7 @@ func (d *Device) requestNewMode(newMode byte) error {
|
||||
if r&modeMask == newMode {
|
||||
return nil
|
||||
} else if e := time.Now(); e.Sub(s) > 200*time.Millisecond {
|
||||
return ErrRequestNewModeMaxTimeEx
|
||||
return errors.New("requestNewMode max time expired")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -865,23 +853,23 @@ func (s *SPI) clearBuffer(dir int) error { return s.setBufferLength(0, dir) }
|
||||
func (s *SPI) setBufferLength(length int, dir int) error {
|
||||
if dir == tx {
|
||||
if length > cap(s.tx) {
|
||||
return ErrLengthIsLongerThanCapacity
|
||||
return fmt.Errorf("length is longer than capacity")
|
||||
}
|
||||
s.tx = s.tx[:length]
|
||||
} else if dir == rx {
|
||||
if length > cap(s.rx) {
|
||||
return ErrLengthIsLongerThanCapacity
|
||||
return fmt.Errorf("length is longer than capacity")
|
||||
}
|
||||
s.rx = s.rx[:length]
|
||||
} else {
|
||||
return ErrInvalidDirection
|
||||
return fmt.Errorf("invalid direction")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SPI) setTxData(data byte) error {
|
||||
if len(s.tx) >= bufferSize {
|
||||
return ErrCannotExpandBuffer
|
||||
return fmt.Errorf("cannot expand buffer (to avoid memory allocation)")
|
||||
}
|
||||
s.tx = append(s.tx, data)
|
||||
|
||||
|
||||
@@ -417,11 +417,4 @@ const (
|
||||
canFail = 0xff
|
||||
|
||||
canMaxCharInMessage = 8
|
||||
|
||||
// for extended id
|
||||
extidTop11WriteMask = 0x1FFC0000
|
||||
extidBottom29Mask = (1 << 29) - 1 // extended id bits
|
||||
extidBottom18Mask = (1 << 18) - 1 // bottom 18 bits
|
||||
stdidBottom11Mask = 0x7FF
|
||||
extidFlagMask = 1 << 19
|
||||
)
|
||||
|
||||
+8
-8
@@ -6,18 +6,18 @@ package pcd8544 // import "tinygo.org/x/drivers/pcd8544"
|
||||
import (
|
||||
"errors"
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// Device wraps an SPI connection.
|
||||
type Device struct {
|
||||
bus drivers.SPI
|
||||
dcPin pin.OutputFunc
|
||||
rstPin pin.OutputFunc
|
||||
scePin pin.OutputFunc
|
||||
dcPin machine.Pin
|
||||
rstPin machine.Pin
|
||||
scePin machine.Pin
|
||||
buffer []byte
|
||||
width int16
|
||||
height int16
|
||||
@@ -30,12 +30,12 @@ type Config struct {
|
||||
}
|
||||
|
||||
// New creates a new PCD8544 connection. The SPI bus must already be configured.
|
||||
func New(bus drivers.SPI, dcPin, rstPin, scePin pin.Output) *Device {
|
||||
func New(bus drivers.SPI, dcPin, rstPin, scePin machine.Pin) *Device {
|
||||
return &Device{
|
||||
bus: bus,
|
||||
dcPin: dcPin.Set,
|
||||
rstPin: rstPin.Set,
|
||||
scePin: scePin.Set,
|
||||
dcPin: dcPin,
|
||||
rstPin: rstPin,
|
||||
scePin: scePin,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -149,20 +149,6 @@ func (img Image[T]) setPixel(index int, c T) {
|
||||
}
|
||||
|
||||
return
|
||||
case zeroColor.BitsPerPixel() == 2:
|
||||
// Grayscale2bit.
|
||||
offset := index / 4 // 4 pixels per byte
|
||||
shift := 6 - (index%4)*2 // bits: 6, 4, 2, 0
|
||||
|
||||
ptr := (*byte)(unsafe.Add(img.data, offset))
|
||||
|
||||
raw := *(*uint8)(unsafe.Pointer(&c))
|
||||
gray := raw & 0b11
|
||||
|
||||
mask := byte(0b11 << shift)
|
||||
*ptr = (*ptr &^ mask) | (gray << shift)
|
||||
return
|
||||
|
||||
case zeroColor.BitsPerPixel()%8 == 0:
|
||||
// Each color starts at a whole byte offset.
|
||||
// This is the easy case.
|
||||
@@ -220,13 +206,6 @@ func (img Image[T]) Get(x, y int) T {
|
||||
ptr := (*byte)(unsafe.Add(img.data, offset))
|
||||
c = ((*ptr >> (7 - uint8(bits))) & 0x1) > 0
|
||||
return any(c).(T)
|
||||
case zeroColor.BitsPerPixel() == 2:
|
||||
// Grayscale2bit.
|
||||
offset := index / 4 // 4 pixels per byte
|
||||
shift := 6 - (index%4)*2 // bits: 6, 4, 2, 0
|
||||
ptr := (*byte)(unsafe.Add(img.data, offset))
|
||||
value := ((*ptr) >> shift) & 0b11
|
||||
return any(Grayscale2bit(value)).(T)
|
||||
case zeroColor.BitsPerPixel()%8 == 0:
|
||||
// Colors like RGB565, RGB888, etc.
|
||||
offset := index * int(unsafe.Sizeof(zeroColor))
|
||||
|
||||
+3
-108
@@ -9,30 +9,9 @@ import (
|
||||
"tinygo.org/x/drivers/pixel"
|
||||
)
|
||||
|
||||
func TestImageRGB888(t *testing.T) {
|
||||
image := pixel.NewImage[pixel.RGB888](5, 3)
|
||||
if width, height := image.Size(); width != 5 || height != 3 {
|
||||
t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height)
|
||||
}
|
||||
for _, c := range []color.RGBA{
|
||||
{R: 0xff, A: 0xff},
|
||||
{G: 0xff, A: 0xff},
|
||||
{B: 0xff, A: 0xff},
|
||||
{R: 0x10, A: 0xff},
|
||||
{G: 0x10, A: 0xff},
|
||||
{B: 0x10, A: 0xff},
|
||||
} {
|
||||
image.Set(4, 2, pixel.NewColor[pixel.RGB888](c.R, c.G, c.B))
|
||||
c2 := image.Get(4, 2).RGBA()
|
||||
if c2 != c {
|
||||
t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageRGB565BE(t *testing.T) {
|
||||
image := pixel.NewImage[pixel.RGB565BE](5, 3)
|
||||
if width, height := image.Size(); width != 5 || height != 3 {
|
||||
if width, height := image.Size(); width != 5 && height != 3 {
|
||||
t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height)
|
||||
}
|
||||
for _, c := range []color.RGBA{
|
||||
@@ -51,30 +30,9 @@ func TestImageRGB565BE(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageRGB555(t *testing.T) {
|
||||
image := pixel.NewImage[pixel.RGB555](5, 3)
|
||||
if width, height := image.Size(); width != 5 || height != 3 {
|
||||
t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height)
|
||||
}
|
||||
for _, c := range []color.RGBA{
|
||||
{R: 0xff, A: 0xff},
|
||||
{G: 0xff, A: 0xff},
|
||||
{B: 0xff, A: 0xff},
|
||||
{R: 0x10, A: 0xff},
|
||||
{G: 0x10, A: 0xff},
|
||||
{B: 0x10, A: 0xff},
|
||||
} {
|
||||
image.Set(4, 2, pixel.NewColor[pixel.RGB555](c.R, c.G, c.B))
|
||||
c2 := image.Get(4, 2).RGBA()
|
||||
if c2 != c {
|
||||
t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageRGB444BE(t *testing.T) {
|
||||
image := pixel.NewImage[pixel.RGB444BE](5, 3)
|
||||
if width, height := image.Size(); width != 5 || height != 3 {
|
||||
if width, height := image.Size(); width != 5 && height != 3 {
|
||||
t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height)
|
||||
}
|
||||
for _, c := range []color.RGBA{
|
||||
@@ -107,69 +65,9 @@ func TestImageRGB444BE(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageGrayscale2bit(t *testing.T) {
|
||||
image := pixel.NewImage[pixel.Grayscale2bit](128, 64)
|
||||
|
||||
if width, height := image.Size(); width != 128 || height != 64 {
|
||||
t.Errorf("image.Size(): expected 128, 64 but got %d, %d", width, height)
|
||||
}
|
||||
|
||||
// Define test colors representing 4 Grayscale levels.
|
||||
testColors := []color.RGBA{
|
||||
{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, // black
|
||||
{R: 0x55, G: 0x55, B: 0x55, A: 0xff}, // dark gray
|
||||
{R: 0xaa, G: 0xaa, B: 0xaa, A: 0xff}, // light gray
|
||||
{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, // white
|
||||
}
|
||||
|
||||
// Single pixel roundtrip test at a fixed coordinate.
|
||||
for _, c := range testColors {
|
||||
encoded := pixel.NewColor[pixel.Grayscale2bit](c.R, c.G, c.B)
|
||||
image.Set(5, 3, encoded)
|
||||
actual := image.Get(5, 3).RGBA()
|
||||
if actual != c {
|
||||
t.Errorf("failed to roundtrip color: expected %v but got %v", c, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-coordinate test across the image.
|
||||
for x := 0; x < 8; x++ {
|
||||
for y, c := range testColors {
|
||||
encoded := pixel.NewColor[pixel.Grayscale2bit](c.R, c.G, c.B)
|
||||
image.Set(x, y, encoded)
|
||||
actual := image.Get(x, y).RGBA()
|
||||
if actual != c {
|
||||
t.Errorf("Set/Get mismatch at (%d,%d): expected %v but got %v", x, y, c, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGrayscale2bitMapping(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input color.RGBA
|
||||
expect pixel.Grayscale2bit
|
||||
}{
|
||||
{color.RGBA{R: 0x00, G: 0x00, B: 0x00}, 0}, // 0
|
||||
{color.RGBA{R: 0x3F, G: 0x3F, B: 0x3F}, 0}, // 63
|
||||
{color.RGBA{R: 0x40, G: 0x40, B: 0x40}, 1}, // 64
|
||||
{color.RGBA{R: 0x7F, G: 0x7F, B: 0x7F}, 1}, // 127
|
||||
{color.RGBA{R: 0x80, G: 0x80, B: 0x80}, 2}, // 128
|
||||
{color.RGBA{R: 0xBF, G: 0xBF, B: 0xBF}, 2}, // 191
|
||||
{color.RGBA{R: 0xC0, G: 0xC0, B: 0xC0}, 3}, // 192
|
||||
{color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF}, 3}, // 255
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
actual := pixel.NewColor[pixel.Grayscale2bit](tc.input.R, tc.input.G, tc.input.B)
|
||||
if actual != tc.expect {
|
||||
t.Errorf("NewGrayscale2bit(%#v) = %d, want %d", tc.input, actual, tc.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageMonochrome(t *testing.T) {
|
||||
image := pixel.NewImage[pixel.Monochrome](128, 64)
|
||||
if width, height := image.Size(); width != 128 || height != 64 {
|
||||
if width, height := image.Size(); width != 128 && height != 64 {
|
||||
t.Errorf("image.Size(): expected 128, 64 but got %d, %d", width, height)
|
||||
}
|
||||
for _, expected := range []color.RGBA{
|
||||
@@ -296,9 +194,6 @@ func TestImageNoise(t *testing.T) {
|
||||
t.Run("RGB444BE", func(t *testing.T) {
|
||||
testImageNoiseN[pixel.RGB444BE](t)
|
||||
})
|
||||
t.Run("Grayscale2bit", func(t *testing.T) {
|
||||
testImageNoiseN[pixel.Grayscale2bit](t)
|
||||
})
|
||||
t.Run("Monochrome", func(t *testing.T) {
|
||||
testImageNoiseN[pixel.Monochrome](t)
|
||||
})
|
||||
|
||||
+4
-34
@@ -16,7 +16,7 @@ import (
|
||||
// particular display. Each pixel is at least 1 byte in size.
|
||||
// The color format is sRGB (or close to it) in all cases except for 1-bit.
|
||||
type Color interface {
|
||||
RGB888 | RGB565BE | RGB555 | RGB444BE | Grayscale2bit | Monochrome
|
||||
RGB888 | RGB565BE | RGB555 | RGB444BE | Monochrome
|
||||
|
||||
BaseColor
|
||||
}
|
||||
@@ -50,8 +50,6 @@ func NewColor[T Color](r, g, b uint8) T {
|
||||
return any(NewRGB555(r, g, b)).(T)
|
||||
case RGB444BE:
|
||||
return any(NewRGB444BE(r, g, b)).(T)
|
||||
case Grayscale2bit:
|
||||
return any(NewGrayscale2bit(r, g, b)).(T)
|
||||
case Monochrome:
|
||||
return any(NewMonochrome(r, g, b)).(T)
|
||||
default:
|
||||
@@ -163,9 +161,9 @@ func (c RGB555) BitsPerPixel() int {
|
||||
|
||||
func (c RGB555) RGBA() color.RGBA {
|
||||
color := color.RGBA{
|
||||
R: (uint8(c) & 0x1F) << 3,
|
||||
G: (uint8(c>>5) & 0x1F) << 3,
|
||||
B: (uint8(c>>10) & 0x1F) << 3,
|
||||
R: uint8(c>>10) << 3,
|
||||
G: uint8(c>>5) << 3,
|
||||
B: uint8(c) << 3,
|
||||
A: 255,
|
||||
}
|
||||
// Correct color rounding, so that 0xff roundtrips back to 0xff.
|
||||
@@ -206,34 +204,6 @@ func (c RGB444BE) RGBA() color.RGBA {
|
||||
return color
|
||||
}
|
||||
|
||||
// Grayscale2bit represents a 2-bit Grayscale value (4 levels: black, dark gray, light gray, white).
|
||||
type Grayscale2bit uint8
|
||||
|
||||
func NewGrayscale2bit(r, g, b uint8) Grayscale2bit {
|
||||
// Convert RGB to luminance using standard weights (approximation of human perception)
|
||||
// Use shift-based operations to reduce processing time.
|
||||
// luminance := (299*uint32(r) + 587*uint32(g) + 114*uint32(b)) / 1000
|
||||
luminance := (77*uint32(r) + 150*uint32(g) + 29*uint32(b)) >> 8
|
||||
// Map to 2-bit value: 0–63 => 0, 64–127 => 1, 128–191 => 2, 192–255 => 3
|
||||
return Grayscale2bit((luminance >> 6) & 0b11)
|
||||
}
|
||||
|
||||
func (c Grayscale2bit) BitsPerPixel() int {
|
||||
return 2
|
||||
}
|
||||
|
||||
func (c Grayscale2bit) RGBA() color.RGBA {
|
||||
// Expand 2-bit Grayscale back to 8-bit (0–255) using multiplication
|
||||
// 0 → 0x00, 1 → 0x55, 2 → 0xAA, 3 → 0xFF (i.e., multiply by 85)
|
||||
gray := uint8(c&0b11) * 85
|
||||
return color.RGBA{
|
||||
R: gray,
|
||||
G: gray,
|
||||
B: gray,
|
||||
A: 255,
|
||||
}
|
||||
}
|
||||
|
||||
type Monochrome bool
|
||||
|
||||
func NewMonochrome(r, g, b uint8) Monochrome {
|
||||
|
||||
@@ -22,5 +22,4 @@ const (
|
||||
CmdStartLowPowerPeriodicMeasurement = 0x21AC
|
||||
CmdStartPeriodicMeasurement = 0x21B1
|
||||
CmdStopPeriodicMeasurement = 0x3F86
|
||||
CmdMeasureSingleShot = 0x219D
|
||||
)
|
||||
|
||||
+8
-46
@@ -82,13 +82,6 @@ func (d *Device) StartLowPowerPeriodicMeasurement() error {
|
||||
return d.sendCommand(CmdStartLowPowerPeriodicMeasurement)
|
||||
}
|
||||
|
||||
// MeasureSingleShot starts a single measurement cycle (SCD41 only). After this
|
||||
// command is complete, the caller should wait for 5000ms before trying to read
|
||||
// the result.
|
||||
func (d *Device) MeasureSingleShot() error {
|
||||
return d.sendCommand(CmdMeasureSingleShot)
|
||||
}
|
||||
|
||||
// ReadData reads the data from the sensor and caches it.
|
||||
func (d *Device) ReadData() error {
|
||||
if err := d.sendCommandWithResult(CmdReadMeasurement, d.rx[0:9]); err != nil {
|
||||
@@ -100,18 +93,7 @@ func (d *Device) ReadData() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update reads new data from the sensor (if new data is available) and caches
|
||||
// it for reading in the CO2, Temperature, and Humidity methods.
|
||||
func (d *Device) Update(measurements drivers.Measurement) error {
|
||||
if measurements&(drivers.Temperature|drivers.Humidity|drivers.Concentration) != 0 {
|
||||
return d.ReadData()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadCO2 returns the CO2 concentration in PPM (parts per million).
|
||||
//
|
||||
// Deprecated: use Update() and CO2() instead.
|
||||
func (d *Device) ReadCO2() (co2 int32, err error) {
|
||||
ok, err := d.DataReady()
|
||||
if err != nil {
|
||||
@@ -123,14 +105,7 @@ func (d *Device) ReadCO2() (co2 int32, err error) {
|
||||
return int32(d.co2), err
|
||||
}
|
||||
|
||||
// CO2 returns last read the CO2 concentration in PPM (parts per million).
|
||||
func (d *Device) CO2() int32 {
|
||||
return int32(d.co2)
|
||||
}
|
||||
|
||||
// ReadTemperature returns the temperature in celsius milli degrees (°C/1000)
|
||||
//
|
||||
// Deprecated: use Update() and Temperature() instead.
|
||||
func (d *Device) ReadTemperature() (temperature int32, err error) {
|
||||
ok, err := d.DataReady()
|
||||
if err != nil {
|
||||
@@ -139,14 +114,8 @@ func (d *Device) ReadTemperature() (temperature int32, err error) {
|
||||
if ok {
|
||||
err = d.ReadData()
|
||||
}
|
||||
return d.Temperature(), err
|
||||
}
|
||||
|
||||
// Temperature returns the last read temperature in celsius milli degrees
|
||||
// (°C/1000).
|
||||
func (d *Device) Temperature() int32 {
|
||||
// temp = -45 + 175 * value / 2¹⁶
|
||||
return (-1 * 45000) + (21875 * (int32(d.temperature)) / 8192)
|
||||
return (-1 * 45000) + (21875 * (int32(d.temperature)) / 8192), err
|
||||
}
|
||||
|
||||
// ReadTempC returns the value in the temperature value in Celsius.
|
||||
@@ -161,11 +130,6 @@ func (d *Device) ReadTempF() float32 {
|
||||
}
|
||||
|
||||
// ReadHumidity returns the current relative humidity in %rH.
|
||||
//
|
||||
// Warning: the value returned here is less precise than the humidity returned
|
||||
// from Humidity()!
|
||||
//
|
||||
// Deprecated: use Update() and Temperature() instead.
|
||||
func (d *Device) ReadHumidity() (humidity int32, err error) {
|
||||
ok, err := d.DataReady()
|
||||
if err != nil {
|
||||
@@ -178,20 +142,18 @@ func (d *Device) ReadHumidity() (humidity int32, err error) {
|
||||
return (25 * int32(d.humidity)) / 16384, err
|
||||
}
|
||||
|
||||
// Humidity returns the relative humidity in hundredths of a percent (in other
|
||||
// words, with a range 0..10_000).
|
||||
//
|
||||
// Warning: the value returned here is of a different scale (more precise) than
|
||||
// ReadHumidity()!
|
||||
func (d *Device) Humidity() int32 {
|
||||
return (2500 * int32(d.humidity)) / 16384
|
||||
}
|
||||
|
||||
func (d *Device) sendCommand(command uint16) error {
|
||||
binary.BigEndian.PutUint16(d.tx[0:], command)
|
||||
return d.bus.Tx(uint16(d.Address), d.tx[0:2], nil)
|
||||
}
|
||||
|
||||
func (d *Device) sendCommandWithValue(command, value uint16) error {
|
||||
binary.BigEndian.PutUint16(d.tx[0:], command)
|
||||
binary.BigEndian.PutUint16(d.tx[2:], value)
|
||||
d.tx[4] = crc8(d.tx[2:4])
|
||||
return d.bus.Tx(uint16(d.Address), d.tx[0:5], nil)
|
||||
}
|
||||
|
||||
func (d *Device) sendCommandWithResult(command uint16, result []byte) error {
|
||||
binary.BigEndian.PutUint16(d.tx[0:], command)
|
||||
if err := d.bus.Tx(uint16(d.Address), d.tx[0:2], nil); err != nil {
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
package si5351
|
||||
|
||||
// The I2C address which this device listens to.
|
||||
const AddressDefault = 0x60 // Assumes ADDR pin is low
|
||||
const AddressAlternative = 0x61 // Assumes ADDR pin is high
|
||||
|
||||
const (
|
||||
XTAL_FREQ = 25000000
|
||||
PLL_FIXED = 80000000000
|
||||
FREQ_MULT = 100
|
||||
DEFAULT_CLK = 1000000000
|
||||
|
||||
PLL_VCO_MIN = 600000000
|
||||
PLL_VCO_MAX = 900000000
|
||||
MULTISYNTH_MIN_FREQ = 500000
|
||||
MULTISYNTH_DIVBY4_FREQ = 150000000
|
||||
MULTISYNTH_MAX_FREQ = 225000000
|
||||
MULTISYNTH_SHARE_MAX = 100000000
|
||||
MULTISYNTH_SHARE_MIN = 1024000
|
||||
MULTISYNTH67_MAX_FREQ = MULTISYNTH_DIVBY4_FREQ
|
||||
CLKOUT_MIN_FREQ = 4000
|
||||
CLKOUT_MAX_FREQ = MULTISYNTH_MAX_FREQ
|
||||
CLKOUT67_MS_MIN = PLL_VCO_MIN / MULTISYNTH67_A_MAX
|
||||
CLKOUT67_MIN_FREQ = CLKOUT67_MS_MIN / 128
|
||||
CLKOUT67_MAX_FREQ = MULTISYNTH67_MAX_FREQ
|
||||
|
||||
PLL_A_MIN = 15
|
||||
PLL_A_MAX = 90
|
||||
PLL_B_MAX = PLL_C_MAX - 1
|
||||
PLL_C_MAX = 1048575
|
||||
MULTISYNTH_A_MIN = 6
|
||||
MULTISYNTH_A_MAX = 1800
|
||||
MULTISYNTH67_A_MAX = 254
|
||||
MULTISYNTH_B_MAX = MULTISYNTH_C_MAX - 1
|
||||
MULTISYNTH_C_MAX = 1048575
|
||||
MULTISYNTH_P1_MAX = (1 << 18) - 1
|
||||
MULTISYNTH_P2_MAX = (1 << 20) - 1
|
||||
MULTISYNTH_P3_MAX = (1 << 20) - 1
|
||||
VCXO_PULL_MIN = 30
|
||||
VCXO_PULL_MAX = 240
|
||||
VCXO_MARGIN = 103
|
||||
|
||||
DEVICE_STATUS = 0
|
||||
INTERRUPT_STATUS = 1
|
||||
INTERRUPT_MASK = 2
|
||||
STATUS_SYS_INIT = 1 << 7
|
||||
STATUS_LOL_B = 1 << 6
|
||||
STATUS_LOL_A = 1 << 5
|
||||
STATUS_LOS = 1 << 4
|
||||
OUTPUT_ENABLE_CTRL = 3
|
||||
OEB_PIN_ENABLE_CTRL = 9
|
||||
PLL_INPUT_SOURCE = 15
|
||||
CLKIN_DIV_MASK = 3 << 6
|
||||
CLKIN_DIV_1 = 0 << 6
|
||||
CLKIN_DIV_2 = 1 << 6
|
||||
CLKIN_DIV_4 = 2 << 6
|
||||
CLKIN_DIV_8 = 3 << 6
|
||||
PLLB_SOURCE = 1 << 3
|
||||
PLLA_SOURCE = 1 << 2
|
||||
|
||||
CLK0_CTRL = 16
|
||||
CLK1_CTRL = 17
|
||||
CLK2_CTRL = 18
|
||||
CLK3_CTRL = 19
|
||||
CLK4_CTRL = 20
|
||||
CLK5_CTRL = 21
|
||||
CLK6_CTRL = 22
|
||||
CLK7_CTRL = 23
|
||||
CLK_POWERDOWN = 1 << 7
|
||||
CLK_INTEGER_MODE = 1 << 6
|
||||
CLK_PLL_SELECT = 1 << 5
|
||||
CLK_INVERT = 1 << 4
|
||||
CLK_INPUT_MASK = 3 << 2
|
||||
CLK_INPUT_XTAL = 0 << 2
|
||||
CLK_INPUT_CLKIN = 1 << 2
|
||||
CLK_INPUT_MULTISYNTH_0_4 = 2 << 2
|
||||
CLK_INPUT_MULTISYNTH_N = 3 << 2
|
||||
CLK_DRIVE_STRENGTH_MASK = 3 << 0
|
||||
CLK_DRIVE_STRENGTH_2MA = 0 << 0
|
||||
CLK_DRIVE_STRENGTH_4MA = 1 << 0
|
||||
CLK_DRIVE_STRENGTH_6MA = 2 << 0
|
||||
CLK_DRIVE_STRENGTH_8MA = 3 << 0
|
||||
|
||||
CLK3_0_DISABLE_STATE = 24
|
||||
CLK7_4_DISABLE_STATE = 25
|
||||
CLK_DISABLE_STATE_MASK = 3
|
||||
CLK_DISABLE_STATE_LOW = 0
|
||||
CLK_DISABLE_STATE_HIGH = 1
|
||||
CLK_DISABLE_STATE_FLOAT = 2
|
||||
CLK_DISABLE_STATE_NEVER = 3
|
||||
|
||||
PARAMETERS_LENGTH = 8
|
||||
PLLA_PARAMETERS = 26
|
||||
PLLB_PARAMETERS = 34
|
||||
CLK0_PARAMETERS = 42
|
||||
CLK1_PARAMETERS = 50
|
||||
CLK2_PARAMETERS = 58
|
||||
CLK3_PARAMETERS = 66
|
||||
CLK4_PARAMETERS = 74
|
||||
CLK5_PARAMETERS = 82
|
||||
CLK6_PARAMETERS = 90
|
||||
CLK7_PARAMETERS = 91
|
||||
CLK6_7_OUTPUT_DIVIDER = 92
|
||||
OUTPUT_CLK_DIV_MASK = 7 << 4
|
||||
OUTPUT_CLK6_DIV_MASK = 7 << 0
|
||||
OUTPUT_CLK_DIV_SHIFT = 4
|
||||
OUTPUT_CLK_DIV6_SHIFT = 0
|
||||
OUTPUT_CLK_DIV_1 = 0
|
||||
OUTPUT_CLK_DIV_2 = 1
|
||||
OUTPUT_CLK_DIV_4 = 2
|
||||
OUTPUT_CLK_DIV_8 = 3
|
||||
OUTPUT_CLK_DIV_16 = 4
|
||||
OUTPUT_CLK_DIV_32 = 5
|
||||
OUTPUT_CLK_DIV_64 = 6
|
||||
OUTPUT_CLK_DIV_128 = 7
|
||||
OUTPUT_CLK_DIVBY4 = 3 << 2
|
||||
|
||||
SSC_PARAM0 = 149
|
||||
SSC_PARAM1 = 150
|
||||
SSC_PARAM2 = 151
|
||||
SSC_PARAM3 = 152
|
||||
SSC_PARAM4 = 153
|
||||
SSC_PARAM5 = 154
|
||||
SSC_PARAM6 = 155
|
||||
SSC_PARAM7 = 156
|
||||
SSC_PARAM8 = 157
|
||||
SSC_PARAM9 = 158
|
||||
SSC_PARAM10 = 159
|
||||
SSC_PARAM11 = 160
|
||||
SSC_PARAM12 = 161
|
||||
|
||||
VXCO_PARAMETERS_LOW = 162
|
||||
VXCO_PARAMETERS_MID = 163
|
||||
VXCO_PARAMETERS_HIGH = 164
|
||||
|
||||
CLK0_PHASE_OFFSET = 165
|
||||
CLK1_PHASE_OFFSET = 166
|
||||
CLK2_PHASE_OFFSET = 167
|
||||
CLK3_PHASE_OFFSET = 168
|
||||
CLK4_PHASE_OFFSET = 169
|
||||
CLK5_PHASE_OFFSET = 170
|
||||
|
||||
PLL_RESET = 177
|
||||
PLL_RESET_B = 1 << 7
|
||||
PLL_RESET_A = 1 << 5
|
||||
|
||||
CRYSTAL_LOAD = 183
|
||||
CRYSTAL_LOAD_MASK = 3 << 6
|
||||
CRYSTAL_LOAD_0PF = 0 << 6
|
||||
CRYSTAL_LOAD_6PF = 1 << 6
|
||||
CRYSTAL_LOAD_8PF = 2 << 6
|
||||
CRYSTAL_LOAD_10PF = 3 << 6
|
||||
|
||||
FANOUT_ENABLE = 187
|
||||
CLKIN_ENABLE = 1 << 7
|
||||
XTAL_ENABLE = 1 << 6
|
||||
MULTISYNTH_ENABLE = 1 << 4
|
||||
)
|
||||
-1069
File diff suppressed because it is too large
Load Diff
@@ -1,214 +0,0 @@
|
||||
package si5351
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSelectRDiv(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
wantDiv uint8
|
||||
wantFreq Frequency
|
||||
}{
|
||||
{"4kHz", 4000 * FREQ_MULT, OUTPUT_CLK_DIV_128, 4000 * FREQ_MULT * 128},
|
||||
{"8kHz", 8000 * FREQ_MULT, OUTPUT_CLK_DIV_64, 8000 * FREQ_MULT * 64},
|
||||
{"16kHz", 16000 * FREQ_MULT, OUTPUT_CLK_DIV_32, 16000 * FREQ_MULT * 32},
|
||||
{"32kHz", 32000 * FREQ_MULT, OUTPUT_CLK_DIV_16, 32000 * FREQ_MULT * 16},
|
||||
{"64kHz", 64000 * FREQ_MULT, OUTPUT_CLK_DIV_8, 64000 * FREQ_MULT * 8},
|
||||
{"128kHz", 128000 * FREQ_MULT, OUTPUT_CLK_DIV_4, 128000 * FREQ_MULT * 4},
|
||||
{"256kHz", 256000 * FREQ_MULT, OUTPUT_CLK_DIV_2, 256000 * FREQ_MULT * 2},
|
||||
{"512kHz", 512000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 512000 * FREQ_MULT},
|
||||
{"1MHz", 1000000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 1000000 * FREQ_MULT},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
freq := tt.freq
|
||||
freq, gotDiv := d.selectRDiv(freq)
|
||||
if gotDiv != tt.wantDiv {
|
||||
t.Errorf("selectRDiv() div = %v, want %v", gotDiv, tt.wantDiv)
|
||||
}
|
||||
if freq != tt.wantFreq {
|
||||
t.Errorf("selectRDiv() freq = %v, want %v", freq, tt.wantFreq)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectRDivMS67(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
wantDiv uint8
|
||||
wantFreq Frequency
|
||||
}{
|
||||
{"4kHz", 4000 * FREQ_MULT, OUTPUT_CLK_DIV_128, 4000 * FREQ_MULT * 128},
|
||||
{"8kHz", 8000 * FREQ_MULT, OUTPUT_CLK_DIV_64, 8000 * FREQ_MULT * 64},
|
||||
{"16kHz", 16000 * FREQ_MULT, OUTPUT_CLK_DIV_32, 16000 * FREQ_MULT * 32},
|
||||
{"64kHz", 64000 * FREQ_MULT, OUTPUT_CLK_DIV_8, 64000 * FREQ_MULT * 8},
|
||||
{"256kHz", 256000 * FREQ_MULT, OUTPUT_CLK_DIV_2, 256000 * FREQ_MULT * 2},
|
||||
{"512kHz", 512000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 512000 * FREQ_MULT},
|
||||
{"1MHz", 1000000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 1000000 * FREQ_MULT},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
freq := tt.freq
|
||||
freq, gotDiv := d.selectRDivMS67(freq)
|
||||
if gotDiv != tt.wantDiv {
|
||||
t.Errorf("selectRDivMS67() div = %v, want %v", gotDiv, tt.wantDiv)
|
||||
}
|
||||
if freq != tt.wantFreq {
|
||||
t.Errorf("selectRDivMS67() freq = %v, want %v", freq, tt.wantFreq)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculatePLL(t *testing.T) {
|
||||
d := &Device{}
|
||||
d.crystalFreq[0] = 25000000
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
wantMin Frequency
|
||||
wantMax Frequency
|
||||
}{
|
||||
{"600MHz", 600000000 * FREQ_MULT, 599000000 * FREQ_MULT, 601000000 * FREQ_MULT},
|
||||
{"750MHz", 750000000 * FREQ_MULT, 749000000 * FREQ_MULT, 751000000 * FREQ_MULT},
|
||||
{"900MHz", 900000000 * FREQ_MULT, 899000000 * FREQ_MULT, 901000000 * FREQ_MULT},
|
||||
{"BelowMin", 500000000 * FREQ_MULT, 600000000 * FREQ_MULT, 600000000 * FREQ_MULT},
|
||||
{"AboveMax", 1000000000 * FREQ_MULT, 900000000 * FREQ_MULT, 900000000 * FREQ_MULT},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, reg := d.CalculatePLL(PLL_A, tt.freq, 0, false)
|
||||
if got < tt.wantMin || got > tt.wantMax {
|
||||
t.Errorf("CalculatePLL() = %v, want between %v and %v", got, tt.wantMin, tt.wantMax)
|
||||
}
|
||||
if reg.p1 == 0 || reg.p3 == 0 {
|
||||
t.Errorf("CalculatePLL() invalid register values: p1=%v, p2=%v, p3=%v", reg.p1, reg.p2, reg.p3)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateMultisynth(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
pllFreq Frequency
|
||||
wantDiv bool
|
||||
}{
|
||||
{"10MHz from 800MHz", 10000000 * FREQ_MULT, 800000000 * FREQ_MULT, false},
|
||||
{"1MHz from 800MHz", 1000000 * FREQ_MULT, 800000000 * FREQ_MULT, false},
|
||||
{"Auto PLL 10MHz", 10000000 * FREQ_MULT, 0, false},
|
||||
{"150MHz DivBy4", 150000000 * FREQ_MULT, 600000000 * FREQ_MULT, true},
|
||||
{"BelowMin", 100000 * FREQ_MULT, 800000000 * FREQ_MULT, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, reg := d.CalculateMultisynth(tt.freq, tt.pllFreq)
|
||||
if tt.pllFreq == 0 {
|
||||
// Auto mode should return a valid PLL frequency
|
||||
if got < PLL_VCO_MIN*FREQ_MULT || got > PLL_VCO_MAX*FREQ_MULT {
|
||||
t.Errorf("CalculateMultisynth() returned invalid PLL freq %v", got)
|
||||
}
|
||||
}
|
||||
if reg.p3 == 0 {
|
||||
t.Errorf("CalculateMultisynth() p3 should not be 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultisynth67Calc(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
pllFreq Frequency
|
||||
wantErr bool
|
||||
}{
|
||||
{"10MHz Auto", 10000000 * FREQ_MULT, 0, false},
|
||||
{"100MHz Auto", 100000000 * FREQ_MULT, 0, false},
|
||||
{"100MHz from 800MHz", 100000000 * FREQ_MULT, 800000000 * FREQ_MULT, false},
|
||||
{"Invalid Division", 10000000 * FREQ_MULT, 777000000 * FREQ_MULT, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, reg := d.multisynth67Calc(tt.freq, tt.pllFreq)
|
||||
if tt.pllFreq == 0 {
|
||||
if got < PLL_VCO_MIN*FREQ_MULT || got > PLL_VCO_MAX*FREQ_MULT {
|
||||
t.Errorf("multisynth67Calc() returned invalid PLL freq %v", got)
|
||||
}
|
||||
} else if tt.wantErr {
|
||||
if got != 0 {
|
||||
t.Errorf("multisynth67Calc() should return 0 for invalid division, got %v", got)
|
||||
}
|
||||
}
|
||||
if reg.p1 == 0 && !tt.wantErr {
|
||||
t.Errorf("multisynth67Calc() p1 should not be 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCorrection(t *testing.T) {
|
||||
// Skip this test as it requires a mock I2C bus
|
||||
t.Skip("Requires mock I2C bus implementation")
|
||||
}
|
||||
|
||||
func TestSetRefFreq(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq CrystalFrequency
|
||||
wantFreq CrystalFrequency
|
||||
wantDiv uint8
|
||||
}{
|
||||
{"25MHz", 25000000, 25000000, CLKIN_DIV_1},
|
||||
{"50MHz", 50000000, 25000000, CLKIN_DIV_2},
|
||||
{"100MHz", 100000000, 25000000, CLKIN_DIV_4},
|
||||
{"30MHz", 30000000, 30000000, CLKIN_DIV_1},
|
||||
{"60MHz", 60000000, 30000000, CLKIN_DIV_2},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d.SetReferenceFrequency(PLLInputClockIn, tt.freq)
|
||||
if d.crystalFreq[PLLInputClockIn] != tt.wantFreq {
|
||||
t.Errorf("SetReferenceFrequency() freq = %v, want %v", d.crystalFreq[PLLInputClockIn], tt.wantFreq)
|
||||
}
|
||||
if d.clkinDiv != tt.wantDiv {
|
||||
t.Errorf("SetReferenceFrequency() clkinDiv = %v, want %v", d.clkinDiv, tt.wantDiv)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCorrection(t *testing.T) {
|
||||
d := &Device{}
|
||||
d.refCorrection[PLLInputXO] = 5000
|
||||
d.refCorrection[PLLInputClockIn] = -3000
|
||||
|
||||
if got := d.GetCorrection(PLLInputXO); got != 5000 {
|
||||
t.Errorf("GetCorrection(PLLInputXO) = %v, want 5000", got)
|
||||
}
|
||||
if got := d.GetCorrection(PLLInputClockIn); got != -3000 {
|
||||
t.Errorf("GetCorrection(PLLInputClockIn) = %v, want -3000", got)
|
||||
}
|
||||
}
|
||||
+1
-7
@@ -20,11 +20,9 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmi
|
||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp180/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp280/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bmp388/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=metro-rp2350 ./examples/bno08x/i2c/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/sram/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/time/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/alarms/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/basic/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/easystepper/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/flash/console/spi
|
||||
tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/flash/console/qspi
|
||||
@@ -124,11 +122,9 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/xpt2046/mai
|
||||
tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/basic/
|
||||
tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/touchpaint/
|
||||
tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/sx126x/lora_rxtx/
|
||||
tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/sx127x/lora_rxtx/
|
||||
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/uc8151/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/waveshare-epd/epd2in9v2/main.go
|
||||
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/scd4x/main.go
|
||||
tinygo build -size short -o ./build/test.uf2 -target=circuitplay-express ./examples/makeybutton/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ds18b20/main.go
|
||||
@@ -148,8 +144,6 @@ tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/mai
|
||||
tinygo build -size short -o ./build/test.uf2 -target=nicenano ./examples/sharpmem/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/max6675/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=pico ./examples/ens160/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=pico ./examples/si5351/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=pico ./examples/w5500/main.go
|
||||
# network examples (espat)
|
||||
tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/
|
||||
# network examples (wifinina)
|
||||
|
||||
+20
-11
@@ -1,19 +1,15 @@
|
||||
package ssd1289
|
||||
|
||||
import (
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
import "machine"
|
||||
|
||||
type pinBus struct {
|
||||
pins [16]pin.Output
|
||||
pins [16]machine.Pin
|
||||
}
|
||||
|
||||
func NewPinBus(pins [16]pin.Output) pinBus {
|
||||
func NewPinBus(pins [16]machine.Pin) pinBus {
|
||||
|
||||
// configure GPIO pins (on baremetal targets only, for backwards compatibility)
|
||||
for i := 0; i < 16; i++ {
|
||||
legacy.ConfigurePinOut(pins[i])
|
||||
pins[i].Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
}
|
||||
|
||||
return pinBus{
|
||||
@@ -22,7 +18,20 @@ func NewPinBus(pins [16]pin.Output) pinBus {
|
||||
}
|
||||
|
||||
func (b pinBus) Set(data uint16) {
|
||||
for i := 15; i >= 0; i-- {
|
||||
b.pins[i].Set((data & (1 << i)) != 0)
|
||||
}
|
||||
b.pins[15].Set((data & (1 << 15)) != 0)
|
||||
b.pins[14].Set((data & (1 << 14)) != 0)
|
||||
b.pins[13].Set((data & (1 << 13)) != 0)
|
||||
b.pins[12].Set((data & (1 << 12)) != 0)
|
||||
b.pins[11].Set((data & (1 << 11)) != 0)
|
||||
b.pins[10].Set((data & (1 << 10)) != 0)
|
||||
b.pins[9].Set((data & (1 << 9)) != 0)
|
||||
b.pins[8].Set((data & (1 << 8)) != 0)
|
||||
b.pins[7].Set((data & (1 << 7)) != 0)
|
||||
b.pins[6].Set((data & (1 << 6)) != 0)
|
||||
b.pins[5].Set((data & (1 << 5)) != 0)
|
||||
b.pins[4].Set((data & (1 << 4)) != 0)
|
||||
b.pins[3].Set((data & (1 << 3)) != 0)
|
||||
b.pins[2].Set((data & (1 << 2)) != 0)
|
||||
b.pins[1].Set((data & (1 << 1)) != 0)
|
||||
b.pins[0].Set((data & (1 << 0)) != 0)
|
||||
}
|
||||
|
||||
+18
-21
@@ -5,10 +5,8 @@ package ssd1289
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
type Bus interface {
|
||||
@@ -16,34 +14,33 @@ type Bus interface {
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
rs pin.OutputFunc
|
||||
wr pin.OutputFunc
|
||||
cs pin.OutputFunc
|
||||
rst pin.OutputFunc
|
||||
rs machine.Pin
|
||||
wr machine.Pin
|
||||
cs machine.Pin
|
||||
rst machine.Pin
|
||||
bus Bus
|
||||
}
|
||||
|
||||
const width = int16(240)
|
||||
const height = int16(320)
|
||||
|
||||
func New(rs, wr, cs, rst pin.Output, bus Bus) *Device {
|
||||
d := &Device{
|
||||
rs: rs.Set,
|
||||
wr: wr.Set,
|
||||
cs: cs.Set,
|
||||
rst: rst.Set,
|
||||
func New(rs machine.Pin, wr machine.Pin, cs machine.Pin, rst machine.Pin, bus Bus) Device {
|
||||
d := Device{
|
||||
rs: rs,
|
||||
wr: wr,
|
||||
cs: cs,
|
||||
rst: rst,
|
||||
bus: bus,
|
||||
}
|
||||
|
||||
// configure GPIO pins (only on baremetal targets, for backwards compatibility)
|
||||
legacy.ConfigurePinOut(rs)
|
||||
legacy.ConfigurePinOut(wr)
|
||||
legacy.ConfigurePinOut(cs)
|
||||
legacy.ConfigurePinOut(rst)
|
||||
rs.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
wr.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
rst.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
|
||||
d.cs.High()
|
||||
d.rst.High()
|
||||
d.wr.High()
|
||||
cs.High()
|
||||
rst.High()
|
||||
wr.High()
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
+12
-14
@@ -1,33 +1,31 @@
|
||||
package ssd1306
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
type SPIBus struct {
|
||||
wire drivers.SPI
|
||||
dcPin pin.OutputFunc
|
||||
resetPin pin.OutputFunc
|
||||
csPin pin.OutputFunc
|
||||
dcPin machine.Pin
|
||||
resetPin machine.Pin
|
||||
csPin machine.Pin
|
||||
buffer []byte // buffer to avoid heap allocations
|
||||
}
|
||||
|
||||
// NewSPI creates a new SSD1306 connection. The SPI wire must already be configured.
|
||||
func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin pin.Output) *Device {
|
||||
// configure GPIO pins (on baremetal targets only, for backwards compatibility)
|
||||
legacy.ConfigurePinOut(dcPin)
|
||||
legacy.ConfigurePinOut(resetPin)
|
||||
legacy.ConfigurePinOut(csPin)
|
||||
func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) *Device {
|
||||
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
return &Device{
|
||||
bus: &SPIBus{
|
||||
wire: bus,
|
||||
dcPin: dcPin.Set,
|
||||
resetPin: resetPin.Set,
|
||||
csPin: csPin.Set,
|
||||
dcPin: dcPin,
|
||||
resetPin: resetPin,
|
||||
csPin: csPin,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -62,7 +60,7 @@ func (b *SPIBus) flush() error {
|
||||
// tx sends data to the display
|
||||
func (b *SPIBus) tx(data []byte, isCommand bool) error {
|
||||
b.csPin.High()
|
||||
b.dcPin(!isCommand)
|
||||
b.dcPin.Set(!isCommand)
|
||||
b.csPin.Low()
|
||||
err := b.wire.Tx(data, nil)
|
||||
b.csPin.High()
|
||||
|
||||
+12
-14
@@ -5,13 +5,12 @@ package ssd1331 // import "tinygo.org/x/drivers/ssd1331"
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
type Model uint8
|
||||
@@ -20,9 +19,9 @@ type Rotation uint8
|
||||
// Device wraps an SPI connection.
|
||||
type Device struct {
|
||||
bus drivers.SPI
|
||||
dcPin pin.OutputFunc
|
||||
resetPin pin.OutputFunc
|
||||
csPin pin.OutputFunc
|
||||
dcPin machine.Pin
|
||||
resetPin machine.Pin
|
||||
csPin machine.Pin
|
||||
width int16
|
||||
height int16
|
||||
batchLength int16
|
||||
@@ -37,16 +36,15 @@ type Config struct {
|
||||
}
|
||||
|
||||
// New creates a new SSD1331 connection. The SPI wire must already be configured.
|
||||
func New(bus drivers.SPI, resetPin, dcPin, csPin pin.Output) Device {
|
||||
// configure GPIO pins (on baremetal targets only, for backwards compatibility)
|
||||
legacy.ConfigurePinOut(dcPin)
|
||||
legacy.ConfigurePinOut(resetPin)
|
||||
legacy.ConfigurePinOut(csPin)
|
||||
func New(bus drivers.SPI, resetPin, dcPin, csPin machine.Pin) Device {
|
||||
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
return Device{
|
||||
bus: bus,
|
||||
dcPin: dcPin.Set,
|
||||
resetPin: resetPin.Set,
|
||||
csPin: csPin.Set,
|
||||
dcPin: dcPin,
|
||||
resetPin: resetPin,
|
||||
csPin: csPin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +251,7 @@ func (d *Device) Data(data uint8) {
|
||||
|
||||
// Tx sends data to the display
|
||||
func (d *Device) Tx(data []byte, isCommand bool) {
|
||||
d.dcPin(!isCommand)
|
||||
d.dcPin.Set(!isCommand)
|
||||
d.bus.Tx(data, nil)
|
||||
}
|
||||
|
||||
|
||||
+25
-30
@@ -6,11 +6,10 @@ package ssd1351 // import "tinygo.org/x/drivers/ssd1351"
|
||||
import (
|
||||
"errors"
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -20,18 +19,17 @@ var (
|
||||
|
||||
// Device wraps an SPI connection.
|
||||
type Device struct {
|
||||
bus drivers.SPI
|
||||
dcPin pin.OutputFunc
|
||||
resetPin pin.OutputFunc
|
||||
csPin pin.OutputFunc
|
||||
enPin pin.OutputFunc
|
||||
rwPin pin.OutputFunc
|
||||
width int16
|
||||
height int16
|
||||
rowOffset int16
|
||||
columnOffset int16
|
||||
bufferLength int16
|
||||
configurePins func()
|
||||
bus drivers.SPI
|
||||
dcPin machine.Pin
|
||||
resetPin machine.Pin
|
||||
csPin machine.Pin
|
||||
enPin machine.Pin
|
||||
rwPin machine.Pin
|
||||
width int16
|
||||
height int16
|
||||
rowOffset int16
|
||||
columnOffset int16
|
||||
bufferLength int16
|
||||
}
|
||||
|
||||
// Config is the configuration for the display
|
||||
@@ -43,21 +41,14 @@ type Config struct {
|
||||
}
|
||||
|
||||
// New creates a new SSD1351 connection. The SPI wire must already be configured.
|
||||
func New(bus drivers.SPI, resetPin, dcPin, csPin, enPin, rwPin pin.Output) Device {
|
||||
func New(bus drivers.SPI, resetPin, dcPin, csPin, enPin, rwPin machine.Pin) Device {
|
||||
return Device{
|
||||
bus: bus,
|
||||
dcPin: dcPin.Set,
|
||||
resetPin: resetPin.Set,
|
||||
csPin: csPin.Set,
|
||||
enPin: enPin.Set,
|
||||
rwPin: rwPin.Set,
|
||||
configurePins: func() {
|
||||
legacy.ConfigurePinOut(dcPin)
|
||||
legacy.ConfigurePinOut(resetPin)
|
||||
legacy.ConfigurePinOut(csPin)
|
||||
legacy.ConfigurePinOut(enPin)
|
||||
legacy.ConfigurePinOut(rwPin)
|
||||
},
|
||||
dcPin: dcPin,
|
||||
resetPin: resetPin,
|
||||
csPin: csPin,
|
||||
enPin: enPin,
|
||||
rwPin: rwPin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +72,12 @@ func (d *Device) Configure(cfg Config) {
|
||||
d.bufferLength = d.height
|
||||
}
|
||||
|
||||
// configure GPIO pins (on baremetal targets only, for backwards compatibility)
|
||||
d.configurePins()
|
||||
// configure GPIO pins
|
||||
d.dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
d.resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
d.csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
d.enPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
d.rwPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
|
||||
// reset the device
|
||||
d.resetPin.High()
|
||||
@@ -283,7 +278,7 @@ func (d *Device) Data(data uint8) {
|
||||
|
||||
// Tx sends data to the display
|
||||
func (d *Device) Tx(data []byte, isCommand bool) {
|
||||
d.dcPin(!isCommand)
|
||||
d.dcPin.Set(!isCommand)
|
||||
d.csPin.Low()
|
||||
d.bus.Tx(data, nil)
|
||||
d.csPin.High()
|
||||
|
||||
+16
-19
@@ -5,13 +5,12 @@ package st7735 // import "tinygo.org/x/drivers/st7735"
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
"tinygo.org/x/drivers/pixel"
|
||||
)
|
||||
|
||||
@@ -40,10 +39,10 @@ type Device = DeviceOf[pixel.RGB565BE]
|
||||
// formats.
|
||||
type DeviceOf[T Color] struct {
|
||||
bus drivers.SPI
|
||||
dcPin pin.OutputFunc
|
||||
resetPin pin.OutputFunc
|
||||
csPin pin.OutputFunc
|
||||
blPin pin.OutputFunc
|
||||
dcPin machine.Pin
|
||||
resetPin machine.Pin
|
||||
csPin machine.Pin
|
||||
blPin machine.Pin
|
||||
width int16
|
||||
height int16
|
||||
columnOffset int16
|
||||
@@ -66,25 +65,23 @@ type Config struct {
|
||||
}
|
||||
|
||||
// New creates a new ST7735 connection. The SPI wire must already be configured.
|
||||
func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin pin.Output) Device {
|
||||
func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device {
|
||||
return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin)
|
||||
}
|
||||
|
||||
// NewOf creates a new ST7735 connection with a particular pixel format. The SPI
|
||||
// wire must already be configured.
|
||||
func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin pin.Output) DeviceOf[T] {
|
||||
// IMPORTANT: pin configuration should really be done outside of this driver,
|
||||
// but for backwards compatibility with existing code, we do it here.
|
||||
legacy.ConfigurePinOut(dcPin)
|
||||
legacy.ConfigurePinOut(resetPin)
|
||||
legacy.ConfigurePinOut(csPin)
|
||||
legacy.ConfigurePinOut(blPin)
|
||||
func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] {
|
||||
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
blPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
return DeviceOf[T]{
|
||||
bus: bus,
|
||||
dcPin: dcPin.Set,
|
||||
resetPin: resetPin.Set,
|
||||
csPin: csPin.Set,
|
||||
blPin: blPin.Set,
|
||||
dcPin: dcPin,
|
||||
resetPin: resetPin,
|
||||
csPin: csPin,
|
||||
blPin: blPin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +423,7 @@ func (d *DeviceOf[T]) Data(data uint8) {
|
||||
|
||||
// Tx sends data to the display
|
||||
func (d *DeviceOf[T]) Tx(data []byte, isCommand bool) {
|
||||
d.dcPin(!isCommand)
|
||||
d.dcPin.Set(!isCommand)
|
||||
d.bus.Tx(data, nil)
|
||||
}
|
||||
|
||||
|
||||
+5
-25
@@ -513,8 +513,8 @@ func (d *DeviceOf[T]) setRotation(rotation Rotation) error {
|
||||
d.columnOffset = 0
|
||||
case drivers.Rotation90:
|
||||
madctl = MADCTL_MX | MADCTL_MV
|
||||
d.rowOffset = d.columnOffsetCfg
|
||||
d.columnOffset = d.rowOffsetCfg
|
||||
d.rowOffset = 0
|
||||
d.columnOffset = 0
|
||||
case drivers.Rotation180:
|
||||
madctl = MADCTL_MX | MADCTL_MY
|
||||
d.rowOffset = d.rowOffsetCfg
|
||||
@@ -593,18 +593,8 @@ func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) {
|
||||
// The screen doesn't use the full 320 pixel height.
|
||||
// Enlarge the bottom fixed area to fill the 320 pixel height, so that
|
||||
// bottomFixedArea starts from the visible bottom of the screen.
|
||||
//
|
||||
// VSCRDEF/VSCRSADD always operate on physical frame memory rows (0-319),
|
||||
// regardless of MADCTL. For rotations with MV set (90°/270°), CASET
|
||||
// addresses physical rows due to row/column exchange, so the physical row
|
||||
// offset is d.columnOffset (= rowOffsetCfg). For other rotations,
|
||||
// d.rowOffset is the physical row offset.
|
||||
physRowOffset := d.rowOffset
|
||||
if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 {
|
||||
physRowOffset = d.columnOffset
|
||||
}
|
||||
topFixedArea += physRowOffset
|
||||
bottomFixedArea += (320 - d.height) - physRowOffset
|
||||
topFixedArea += d.rowOffset
|
||||
bottomFixedArea += (320 - d.height) - d.rowOffset
|
||||
}
|
||||
if d.rotation == drivers.Rotation180 {
|
||||
// The screen is rotated by 180°, so we have to switch the top and
|
||||
@@ -623,20 +613,10 @@ func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) {
|
||||
|
||||
// SetScroll sets the vertical scroll address of the display.
|
||||
func (d *DeviceOf[T]) SetScroll(line int16) {
|
||||
switch d.rotation {
|
||||
case drivers.Rotation90:
|
||||
// With MV set, hardware scroll operates on physical rows, which map to the
|
||||
// visual X axis. Add the physical row offset (d.columnOffset = rowOffsetCfg)
|
||||
// so that line=0 addresses the first visible physical row.
|
||||
line = line + d.columnOffset
|
||||
case drivers.Rotation180:
|
||||
if d.rotation == drivers.Rotation180 {
|
||||
// The screen is rotated by 180°, so we have to invert the scroll line
|
||||
// (taking care of the RowOffset).
|
||||
line = (319 - d.rowOffset) - line
|
||||
case drivers.Rotation270:
|
||||
// With MV+MY, physical rows map to the visual X axis in reverse direction.
|
||||
// line=0 addresses the last physical row of the visible area.
|
||||
line = (d.columnOffset + d.height - 1) - line
|
||||
}
|
||||
d.buf[0] = uint8(line >> 8)
|
||||
d.buf[1] = uint8(line)
|
||||
|
||||
@@ -96,12 +96,6 @@ const (
|
||||
SX127X_OPMODE_RX_SINGLE = uint8(0x06)
|
||||
SX127X_OPMODE_CAD = uint8(0x07)
|
||||
|
||||
SX127X_OPMODE_LOW_FREQUENCY = uint8(0x4)
|
||||
|
||||
SX127X_OPMODE_MODULATION_MASK = uint8(0x60)
|
||||
SX127X_OPMODE_MODULATION_FSK = uint8(0x0)
|
||||
SX127X_OPMODE_MODULATION_OOK = uint8(0x20)
|
||||
|
||||
SX127X_LORA_MAC_PUBLIC_SYNCWORD = 0x34
|
||||
SX127X_LORA_MAC_PRIVATE_SYNCWORD = 0x14
|
||||
)
|
||||
|
||||
+11
-38
@@ -25,9 +25,9 @@ type Device struct {
|
||||
rstPin machine.Pin // GPIO for reset
|
||||
radioEventChan chan lora.RadioEvent // Channel for Receiving events
|
||||
loraConf lora.Config // Current Lora configuration
|
||||
controller RadioController // to manage interrupts with the radio
|
||||
controller RadioController // to manage interactions with the radio
|
||||
deepSleep bool // Internal Sleep state
|
||||
deviceType int // sx1272, sx1273, sx1276, sx1279 (defaults sx1276)
|
||||
deviceType int // sx1261,sx1262,sx1268 (defaults sx1261)
|
||||
spiTxBuf []byte // global Tx buffer to avoid heap allocations in interrupt
|
||||
spiRxBuf []byte // global Rx buffer to avoid heap allocations in interrupt
|
||||
}
|
||||
@@ -65,11 +65,6 @@ func (d *Device) SetRadioController(rc RadioController) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Specify device type (sx1272, sx1273, sx1276, sx1279)
|
||||
func (d *Device) SetDeviceType(devType int) {
|
||||
d.deviceType = devType
|
||||
}
|
||||
|
||||
// Reset re-initialize the sx127x device
|
||||
func (d *Device) Reset() {
|
||||
d.rstPin.Low()
|
||||
@@ -86,11 +81,9 @@ func (d *Device) DetectDevice() bool {
|
||||
|
||||
// ReadRegister reads register value
|
||||
func (d *Device) ReadRegister(reg uint8) uint8 {
|
||||
if d.controller != nil {
|
||||
d.controller.SetNss(false)
|
||||
}
|
||||
|
||||
d.controller.SetNss(false)
|
||||
// Send register
|
||||
//d.spiTxBuf = []byte{reg & 0x7f}
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, byte(reg&0x7f))
|
||||
d.spi.Tx(d.spiTxBuf, nil)
|
||||
@@ -98,19 +91,13 @@ func (d *Device) ReadRegister(reg uint8) uint8 {
|
||||
d.spiRxBuf = d.spiRxBuf[:0]
|
||||
d.spiRxBuf = append(d.spiRxBuf, 0)
|
||||
d.spi.Tx(nil, d.spiRxBuf)
|
||||
if d.controller != nil {
|
||||
d.controller.SetNss(true)
|
||||
}
|
||||
|
||||
d.controller.SetNss(true)
|
||||
return d.spiRxBuf[0]
|
||||
}
|
||||
|
||||
// WriteRegister writes value to register
|
||||
func (d *Device) WriteRegister(reg uint8, value uint8) uint8 {
|
||||
if d.controller != nil {
|
||||
d.controller.SetNss(false)
|
||||
}
|
||||
|
||||
d.controller.SetNss(false)
|
||||
// Send register
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, byte(reg|0x80))
|
||||
@@ -121,10 +108,7 @@ func (d *Device) WriteRegister(reg uint8, value uint8) uint8 {
|
||||
d.spiRxBuf = d.spiRxBuf[:0]
|
||||
d.spiRxBuf = append(d.spiRxBuf, 0)
|
||||
d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
if d.controller != nil {
|
||||
d.controller.SetNss(true)
|
||||
}
|
||||
|
||||
d.controller.SetNss(true)
|
||||
return d.spiRxBuf[0]
|
||||
}
|
||||
|
||||
@@ -135,20 +119,9 @@ func (d *Device) SetOpMode(mode uint8) {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, new)
|
||||
}
|
||||
|
||||
// SetOpModeLora changes the sx1276 mode to lora.
|
||||
// SetOpMode changes the sx1276 mode
|
||||
func (d *Device) SetOpModeLora() {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|SX127X_OPMODE_LORA)
|
||||
}
|
||||
|
||||
// SetOpModeFsk changes the sx1276 mode to fsk/ook.
|
||||
func (d *Device) SetOpModeFsk() {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&^SX127X_OPMODE_LORA)
|
||||
}
|
||||
|
||||
// SetModulationType changes the modulation type (SX127X_OPMODE_MODULATION_FSK, SX127X_OPMODE_MODULATION_OOK)
|
||||
func (d *Device) SetModulationType(typ uint8) {
|
||||
cleared := d.ReadRegister(SX127X_REG_OP_MODE) &^ SX127X_OPMODE_MODULATION_MASK
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, cleared|typ)
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, SX127X_OPMODE_LORA)
|
||||
}
|
||||
|
||||
// GetVersion returns hardware version of sx1276 chipset
|
||||
@@ -271,9 +244,9 @@ func (d *Device) SetLowDataRateOptim(val uint8) {
|
||||
// SetLowFrequencyModeOn enables Low Data Rate Optimization
|
||||
func (d *Device) SetLowFrequencyModeOn(val bool) {
|
||||
if val {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|SX127X_OPMODE_LOW_FREQUENCY)
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|0x04)
|
||||
} else {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&^SX127X_OPMODE_LOW_FREQUENCY)
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&0xfb)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
// Package unoqmatrix provides a driver for the UnoQMatrix LED matrix display.
|
||||
//
|
||||
// The UnoQMatrix is an 8x13 LED matrix display that can be controlled using a single pin.
|
||||
// It uses a multiplexing technique to control the LEDs, which allows for a large number of LEDs to be controlled with fewer pins.
|
||||
//
|
||||
// This driver provides basic functionality to set individual pixels, clear the display, and refresh the display.
|
||||
//
|
||||
// Note: The UnoQMatrix does not support brightness control or color depth. Each pixel can only be turned on or off.
|
||||
// Could it suppport brightness control by using PWM on the pin? To be investigated.
|
||||
package unoqmatrix
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
pin "tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Rotation of the LED matrix.
|
||||
Rotation uint8
|
||||
}
|
||||
|
||||
// Valid values:
|
||||
//
|
||||
// 0: regular orientation, (0 degree rotation)
|
||||
// 1: 90 degree rotation clockwise
|
||||
// 2: 180 degree rotation clockwise
|
||||
// 3: 270 degree rotation clockwise
|
||||
const (
|
||||
RotationNormal = 0
|
||||
Rotation90 = 1
|
||||
Rotation180 = 2
|
||||
Rotation270 = 3
|
||||
)
|
||||
|
||||
const (
|
||||
ledRows = 8
|
||||
ledCols = 13
|
||||
|
||||
pixelRefreshDelay = 10 * time.Microsecond
|
||||
)
|
||||
|
||||
// CharlieplexPin represents a pin used for charlieplexing.
|
||||
// It must be able to drive high/low (output mode) and float (high-impedance/input mode).
|
||||
//
|
||||
// Example construction from a machine.Pin using the pin HAL pattern:
|
||||
//
|
||||
// var isOutput bool
|
||||
// cp := unoqmatrix.CharlieplexPin{
|
||||
// Set: pin.OutputFunc(func(level bool) {
|
||||
// if !isOutput {
|
||||
// p.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
// isOutput = true
|
||||
// }
|
||||
// p.Set(level)
|
||||
// }),
|
||||
// Float: func() {
|
||||
// if isOutput {
|
||||
// p.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
// isOutput = false
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
type CharlieplexPin struct {
|
||||
Set pin.OutputFunc // Drive pin high (true) or low (false); auto-configures to output mode.
|
||||
Float func() // Put pin into high-impedance (input) mode.
|
||||
}
|
||||
|
||||
const numPins = 11
|
||||
|
||||
// Device represents the UnoQMatrix LED matrix display.
|
||||
type Device struct {
|
||||
pins [numPins]CharlieplexPin
|
||||
buffer [ledRows][ledCols]color.RGBA
|
||||
rotation uint8
|
||||
}
|
||||
|
||||
// New returns a new unoqmatrix driver.
|
||||
// The provided pins are the 11 charlieplex pins used to control the LED matrix.
|
||||
func New(pins [numPins]CharlieplexPin) Device {
|
||||
return Device{pins: pins}
|
||||
}
|
||||
|
||||
// Configure sets up the device.
|
||||
func (d *Device) Configure(cfg Config) {
|
||||
d.SetRotation(cfg.Rotation)
|
||||
}
|
||||
|
||||
// SetRotation changes the rotation of the LED matrix.
|
||||
//
|
||||
// Valid values for rotation:
|
||||
//
|
||||
// 0: regular orientation, (0 degree rotation)
|
||||
// 1: 90 degree rotation clockwise
|
||||
// 2: 180 degree rotation clockwise
|
||||
// 3: 270 degree rotation clockwise
|
||||
func (d *Device) SetRotation(rotation uint8) {
|
||||
d.rotation = rotation % 4
|
||||
}
|
||||
|
||||
// SetPixel sets the color of a specific pixel.
|
||||
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
|
||||
d.buffer[y][x] = c
|
||||
}
|
||||
|
||||
// GetPixel returns the color of a specific pixel.
|
||||
func (d *Device) GetPixel(x int16, y int16) color.RGBA {
|
||||
return d.buffer[y][x]
|
||||
}
|
||||
|
||||
// Display sends the buffer (if any) to the screen.
|
||||
// Only lights active (non-black) pixels, and resets only the 2 previously
|
||||
// driven pins between LEDs instead of all 11, making each refresh cycle
|
||||
// proportional to the number of lit LEDs.
|
||||
func (d *Device) Display() error {
|
||||
d.clearDisplay()
|
||||
|
||||
var lastIdx0, lastIdx1 uint8
|
||||
hasLast := false
|
||||
|
||||
for row := 0; row < ledRows; row++ {
|
||||
for col := 0; col < ledCols; col++ {
|
||||
c := d.buffer[row][col]
|
||||
if c.R == 0 && c.G == 0 && c.B == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
idx := row*ledCols + col
|
||||
if idx < 0 || idx >= len(pinMapping) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Float only the two pins that were driving the previous LED.
|
||||
if hasLast {
|
||||
d.pins[lastIdx0].Float()
|
||||
d.pins[lastIdx1].Float()
|
||||
}
|
||||
hasLast = true
|
||||
|
||||
idx0 := pinMapping[idx][0]
|
||||
idx1 := pinMapping[idx][1]
|
||||
d.pins[idx0].Set.High()
|
||||
d.pins[idx1].Set.Low()
|
||||
lastIdx0 = idx0
|
||||
lastIdx1 = idx1
|
||||
|
||||
time.Sleep(pixelRefreshDelay)
|
||||
}
|
||||
}
|
||||
|
||||
// Float the last driven LED.
|
||||
if hasLast {
|
||||
d.pins[lastIdx0].Float()
|
||||
d.pins[lastIdx1].Float()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearDisplay turns off all the LEDs on the display.
|
||||
func (d *Device) ClearDisplay() {
|
||||
for row := 0; row < ledRows; row++ {
|
||||
for col := 0; col < ledCols; col++ {
|
||||
d.buffer[row][col] = color.RGBA{0, 0, 0, 255}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the current size of the display.
|
||||
func (d *Device) Size() (w, h int16) {
|
||||
return ledCols, ledRows
|
||||
}
|
||||
|
||||
// pinMapping defines the mapping of LED indices to pin pairs. Each entry corresponds
|
||||
// to an LED index (0-104) and contains the two pin numbers that need to be set to turn on that LED.
|
||||
// based on https://github.com/arduino/ArduinoCore-zephyr/blob/main/loader/matrix.inc#L13
|
||||
var pinMapping = [][2]uint8{
|
||||
{0, 1}, // 0
|
||||
{1, 0},
|
||||
{0, 2},
|
||||
{2, 0},
|
||||
{1, 2},
|
||||
{2, 1},
|
||||
{0, 3},
|
||||
{3, 0},
|
||||
{1, 3},
|
||||
{3, 1},
|
||||
{2, 3}, // 10
|
||||
{3, 2},
|
||||
{0, 4},
|
||||
{4, 0},
|
||||
{1, 4},
|
||||
{4, 1},
|
||||
{2, 4},
|
||||
{4, 2},
|
||||
{3, 4},
|
||||
{4, 3},
|
||||
{0, 5}, // 20
|
||||
{5, 0},
|
||||
{1, 5},
|
||||
{5, 1},
|
||||
{2, 5},
|
||||
{5, 2},
|
||||
{3, 5},
|
||||
{5, 3},
|
||||
{4, 5},
|
||||
{5, 4},
|
||||
{0, 6}, // 30
|
||||
{6, 0},
|
||||
{1, 6},
|
||||
{6, 1},
|
||||
{2, 6},
|
||||
{6, 2},
|
||||
{3, 6},
|
||||
{6, 3},
|
||||
{4, 6},
|
||||
{6, 4},
|
||||
{5, 6}, // 40
|
||||
{6, 5},
|
||||
{0, 7},
|
||||
{7, 0},
|
||||
{1, 7},
|
||||
{7, 1},
|
||||
{2, 7},
|
||||
{7, 2},
|
||||
{3, 7},
|
||||
{7, 3},
|
||||
{4, 7}, // 50
|
||||
{7, 4},
|
||||
{5, 7},
|
||||
{7, 5},
|
||||
{6, 7},
|
||||
{7, 6},
|
||||
{0, 8},
|
||||
{8, 0},
|
||||
{1, 8},
|
||||
{8, 1},
|
||||
{2, 8}, // 60
|
||||
{8, 2},
|
||||
{3, 8},
|
||||
{8, 3},
|
||||
{4, 8},
|
||||
{8, 4},
|
||||
{5, 8},
|
||||
{8, 5},
|
||||
{6, 8},
|
||||
{8, 6},
|
||||
{7, 8}, // 70
|
||||
{8, 7},
|
||||
{0, 9},
|
||||
{9, 0},
|
||||
{1, 9},
|
||||
{9, 1},
|
||||
{2, 9},
|
||||
{9, 2},
|
||||
{3, 9},
|
||||
{9, 3},
|
||||
{4, 9}, // 80
|
||||
{9, 4},
|
||||
{5, 9},
|
||||
{9, 5},
|
||||
{6, 9},
|
||||
{9, 6},
|
||||
{7, 9},
|
||||
{9, 7},
|
||||
{8, 9},
|
||||
{9, 8},
|
||||
{0, 10}, // 90
|
||||
{10, 0},
|
||||
{1, 10},
|
||||
{10, 1},
|
||||
{2, 10},
|
||||
{10, 2},
|
||||
{3, 10},
|
||||
{10, 3},
|
||||
{4, 10},
|
||||
{10, 4},
|
||||
{5, 10}, // 100
|
||||
{10, 5},
|
||||
{6, 10},
|
||||
{10, 6},
|
||||
}
|
||||
|
||||
// clearDisplay turns off all the LEDs on the display by floating all pins.
|
||||
func (d *Device) clearDisplay() {
|
||||
for i := range d.pins {
|
||||
d.pins[i].Float()
|
||||
}
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
package unoqmatrix
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"testing"
|
||||
|
||||
pin "tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// pinState tracks the state of a mock charlieplex pin.
|
||||
type pinState struct {
|
||||
level bool // true=high, false=low
|
||||
isOutput bool // true=output mode, false=floating (high-Z)
|
||||
}
|
||||
|
||||
// mockPins creates 11 mock CharlieplexPins and returns them along with their observable state.
|
||||
func mockPins() ([numPins]CharlieplexPin, *[numPins]pinState) {
|
||||
var pins [numPins]CharlieplexPin
|
||||
var states [numPins]pinState
|
||||
for i := range pins {
|
||||
idx := i // capture
|
||||
pins[i] = CharlieplexPin{
|
||||
Set: pin.OutputFunc(func(level bool) {
|
||||
states[idx].isOutput = true
|
||||
states[idx].level = level
|
||||
}),
|
||||
Float: func() {
|
||||
states[idx].isOutput = false
|
||||
states[idx].level = false
|
||||
},
|
||||
}
|
||||
}
|
||||
return pins, &states
|
||||
}
|
||||
|
||||
func newTestDevice() (Device, *[numPins]pinState) {
|
||||
pins, states := mockPins()
|
||||
d := New(pins)
|
||||
return d, states
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
w, h := d.Size()
|
||||
if w != ledCols || h != ledRows {
|
||||
t.Errorf("Size() = (%d, %d), want (%d, %d)", w, h, ledCols, ledRows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
w, h := d.Size()
|
||||
if w != 13 {
|
||||
t.Errorf("width = %d, want 13", w)
|
||||
}
|
||||
if h != 8 {
|
||||
t.Errorf("height = %d, want 8", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGetPixel(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
c := color.RGBA{R: 255, G: 128, B: 64, A: 255}
|
||||
|
||||
d.SetPixel(3, 2, c)
|
||||
got := d.GetPixel(3, 2)
|
||||
if got != c {
|
||||
t.Errorf("GetPixel(3,2) = %v, want %v", got, c)
|
||||
}
|
||||
|
||||
// Unset pixel should be zero-value.
|
||||
got = d.GetPixel(0, 0)
|
||||
if got != (color.RGBA{}) {
|
||||
t.Errorf("GetPixel(0,0) = %v, want zero", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearDisplay(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
on := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
off := color.RGBA{A: 255}
|
||||
|
||||
d.SetPixel(0, 0, on)
|
||||
d.SetPixel(5, 3, on)
|
||||
d.ClearDisplay()
|
||||
|
||||
for y := int16(0); y < ledRows; y++ {
|
||||
for x := int16(0); x < ledCols; x++ {
|
||||
got := d.GetPixel(x, y)
|
||||
if got != off {
|
||||
t.Errorf("after ClearDisplay, GetPixel(%d,%d) = %v, want %v", x, y, got, off)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRotation(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
|
||||
tests := []struct {
|
||||
input uint8
|
||||
want uint8
|
||||
}{
|
||||
{0, 0},
|
||||
{1, 1},
|
||||
{2, 2},
|
||||
{3, 3},
|
||||
{4, 0}, // wraps
|
||||
{7, 3}, // wraps
|
||||
}
|
||||
for _, tt := range tests {
|
||||
d.SetRotation(tt.input)
|
||||
if d.rotation != tt.want {
|
||||
t.Errorf("SetRotation(%d): rotation = %d, want %d", tt.input, d.rotation, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
d.Configure(Config{Rotation: 2})
|
||||
if d.rotation != 2 {
|
||||
t.Errorf("Configure(Rotation:2): rotation = %d, want 2", d.rotation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayEmptyBuffer(t *testing.T) {
|
||||
d, states := newTestDevice()
|
||||
|
||||
err := d.Display()
|
||||
if err != nil {
|
||||
t.Fatalf("Display() error: %v", err)
|
||||
}
|
||||
|
||||
// All pins should be floating after displaying an empty buffer.
|
||||
for i, s := range states {
|
||||
if s.isOutput {
|
||||
t.Errorf("pin %d still in output mode after empty Display()", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplaySinglePixel(t *testing.T) {
|
||||
d, states := newTestDevice()
|
||||
on := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
|
||||
// LED index 0 -> pinMapping[0] = {0, 1}: pin 0 high, pin 1 low.
|
||||
d.SetPixel(0, 0, on)
|
||||
err := d.Display()
|
||||
if err != nil {
|
||||
t.Fatalf("Display() error: %v", err)
|
||||
}
|
||||
|
||||
// After Display completes, all pins should be floating (last LED turned off).
|
||||
for i, s := range states {
|
||||
if s.isOutput {
|
||||
t.Errorf("pin %d still in output mode after Display()", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayMultiplePixels(t *testing.T) {
|
||||
d, states := newTestDevice()
|
||||
on := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
|
||||
d.SetPixel(0, 0, on) // idx 0 -> pins {0,1}
|
||||
d.SetPixel(1, 0, on) // idx 1 -> pins {1,0}
|
||||
d.SetPixel(2, 0, on) // idx 2 -> pins {0,2}
|
||||
|
||||
err := d.Display()
|
||||
if err != nil {
|
||||
t.Fatalf("Display() error: %v", err)
|
||||
}
|
||||
|
||||
// All pins floating after display completes.
|
||||
for i, s := range states {
|
||||
if s.isOutput {
|
||||
t.Errorf("pin %d still in output mode after Display()", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pinEvent records a single pin action during Display().
|
||||
type pinEvent struct {
|
||||
pinIdx int
|
||||
action string // "high", "low", or "float"
|
||||
}
|
||||
|
||||
// traceDevice creates a device that records every pin event for verification.
|
||||
func traceDevice() (Device, *[]pinEvent) {
|
||||
var pins [numPins]CharlieplexPin
|
||||
events := &[]pinEvent{}
|
||||
for i := range pins {
|
||||
idx := i
|
||||
pins[i] = CharlieplexPin{
|
||||
Set: pin.OutputFunc(func(level bool) {
|
||||
action := "low"
|
||||
if level {
|
||||
action = "high"
|
||||
}
|
||||
*events = append(*events, pinEvent{pinIdx: idx, action: action})
|
||||
}),
|
||||
Float: func() {
|
||||
*events = append(*events, pinEvent{pinIdx: idx, action: "float"})
|
||||
},
|
||||
}
|
||||
}
|
||||
d := New(pins)
|
||||
return d, events
|
||||
}
|
||||
|
||||
func TestDisplayDrivesCorrectPins(t *testing.T) {
|
||||
d, events := traceDevice()
|
||||
on := color.RGBA{R: 255, A: 255}
|
||||
|
||||
// Set pixel at (0,0) -> LED index 0 -> pinMapping[0] = {0, 1}.
|
||||
d.SetPixel(0, 0, on)
|
||||
d.Display()
|
||||
|
||||
// Expected sequence:
|
||||
// 1. clearDisplay: float pins 0..10
|
||||
// 2. Drive LED 0: pin 0 high, pin 1 low
|
||||
// 3. Cleanup: float pin 0, float pin 1
|
||||
|
||||
// Find the high/low events (skip initial floats from clearDisplay).
|
||||
var driveEvents []pinEvent
|
||||
for _, e := range *events {
|
||||
if e.action == "high" || e.action == "low" {
|
||||
driveEvents = append(driveEvents, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(driveEvents) != 2 {
|
||||
t.Fatalf("expected 2 drive events, got %d: %v", len(driveEvents), driveEvents)
|
||||
}
|
||||
if driveEvents[0].pinIdx != 0 || driveEvents[0].action != "high" {
|
||||
t.Errorf("first drive event = %v, want pin 0 high", driveEvents[0])
|
||||
}
|
||||
if driveEvents[1].pinIdx != 1 || driveEvents[1].action != "low" {
|
||||
t.Errorf("second drive event = %v, want pin 1 low", driveEvents[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplaySkipsBlackPixels(t *testing.T) {
|
||||
d, events := traceDevice()
|
||||
on := color.RGBA{R: 255, A: 255}
|
||||
|
||||
// Only set one pixel in the middle of the matrix.
|
||||
d.SetPixel(4, 1, on) // idx = 1*13+4 = 17 -> pinMapping[17] = {4,2}
|
||||
d.Display()
|
||||
|
||||
var driveEvents []pinEvent
|
||||
for _, e := range *events {
|
||||
if e.action == "high" || e.action == "low" {
|
||||
driveEvents = append(driveEvents, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Should only drive one LED's worth of pin events.
|
||||
if len(driveEvents) != 2 {
|
||||
t.Fatalf("expected 2 drive events for 1 lit pixel, got %d", len(driveEvents))
|
||||
}
|
||||
if driveEvents[0].pinIdx != 4 || driveEvents[0].action != "high" {
|
||||
t.Errorf("expected pin 4 high, got %v", driveEvents[0])
|
||||
}
|
||||
if driveEvents[1].pinIdx != 2 || driveEvents[1].action != "low" {
|
||||
t.Errorf("expected pin 2 low, got %v", driveEvents[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayFloatsBetweenLEDs(t *testing.T) {
|
||||
d, events := traceDevice()
|
||||
on := color.RGBA{R: 255, A: 255}
|
||||
|
||||
d.SetPixel(0, 0, on) // idx 0 -> {0,1}
|
||||
d.SetPixel(1, 0, on) // idx 1 -> {1,0}
|
||||
d.Display()
|
||||
|
||||
// After the initial clearDisplay floats, the sequence for two LEDs should be:
|
||||
// drive LED0 (pin0 high, pin1 low)
|
||||
// float pin0, float pin1 (between LEDs)
|
||||
// drive LED1 (pin1 high, pin0 low)
|
||||
// float pin1, float pin0 (cleanup)
|
||||
|
||||
// Skip the initial 11 float events from clearDisplay.
|
||||
postClear := (*events)[numPins:]
|
||||
|
||||
// Verify pin 0 and 1 are floated between the two LEDs.
|
||||
foundFloatBetween := false
|
||||
driveCount := 0
|
||||
for _, e := range postClear {
|
||||
if e.action == "high" || e.action == "low" {
|
||||
driveCount++
|
||||
}
|
||||
// After the first pair of drive events, we should see floats before the next pair.
|
||||
if driveCount == 2 && e.action == "float" {
|
||||
foundFloatBetween = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundFloatBetween {
|
||||
t.Error("expected float events between LED drives, found none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinMappingLength(t *testing.T) {
|
||||
expected := 104 // 8x13 matrix = 104 LEDs
|
||||
if len(pinMapping) != expected {
|
||||
t.Errorf("pinMapping has %d entries, want %d", len(pinMapping), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinMappingIndicesInRange(t *testing.T) {
|
||||
for i, pair := range pinMapping {
|
||||
if pair[0] >= numPins {
|
||||
t.Errorf("pinMapping[%d][0] = %d, exceeds numPins (%d)", i, pair[0], numPins)
|
||||
}
|
||||
if pair[1] >= numPins {
|
||||
t.Errorf("pinMapping[%d][1] = %d, exceeds numPins (%d)", i, pair[1], numPins)
|
||||
}
|
||||
if pair[0] == pair[1] {
|
||||
t.Errorf("pinMapping[%d] has same pin for both: %d", i, pair[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
//go:build baremetal
|
||||
|
||||
package unoqmatrix
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
pin "tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// NewFromBasePin creates a Device from a base machine.Pin.
|
||||
// It constructs 11 CharlieplexPin values from consecutive pins starting at basePin.
|
||||
// Each pin lazily switches between output and input mode as needed.
|
||||
func NewFromBasePin(basePin machine.Pin) Device {
|
||||
var pins [numPins]CharlieplexPin
|
||||
for i := range pins {
|
||||
p := basePin + machine.Pin(i)
|
||||
var isOutput bool
|
||||
pins[i] = CharlieplexPin{
|
||||
Set: pin.OutputFunc(func(level bool) {
|
||||
if !isOutput {
|
||||
p.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
isOutput = true
|
||||
}
|
||||
p.Set(level)
|
||||
}),
|
||||
Float: func() {
|
||||
if isOutput {
|
||||
p.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
isOutput = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return New(pins)
|
||||
}
|
||||
+1
-1
@@ -2,4 +2,4 @@ package drivers
|
||||
|
||||
// Version returns a user-readable string showing the version of the drivers package for support purposes.
|
||||
// Update this value before release of new version of software.
|
||||
const Version = "0.35.0"
|
||||
const Version = "0.33.0"
|
||||
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
package w5500
|
||||
|
||||
import "time"
|
||||
|
||||
func (d *Device) irqPoll(sockn uint8, state uint8, deadline time.Time) uint8 {
|
||||
waitTime := 500 * time.Microsecond
|
||||
for {
|
||||
if !deadline.IsZero() && time.Now().After(deadline) {
|
||||
// If a deadline is set and it has passed, return 0.
|
||||
return sockIntUnknown
|
||||
}
|
||||
|
||||
irq := d.readByte(sockInt, sockAddr(sockn)) & 0b00011111
|
||||
if got := irq & state; got != 0 {
|
||||
// Acknowledge the interrupt.
|
||||
d.writeByte(sockInt, sockAddr(sockn), got)
|
||||
|
||||
return got
|
||||
}
|
||||
|
||||
d.mu.Unlock()
|
||||
|
||||
time.Sleep(waitTime)
|
||||
|
||||
// Exponential backoff for polling.
|
||||
waitTime *= 2
|
||||
if waitTime > 10*time.Millisecond {
|
||||
waitTime = 10 * time.Millisecond
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) read(addr uint16, bsb uint8, p []byte) {
|
||||
d.cs(false)
|
||||
if len(p) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
d.sendReadHeader(addr, bsb)
|
||||
_ = d.bus.Tx(nil, p)
|
||||
d.cs(true)
|
||||
}
|
||||
|
||||
func (d *Device) readUint16(addr uint16, bsb uint8) uint16 {
|
||||
d.cs(false)
|
||||
d.sendReadHeader(addr, bsb)
|
||||
buf := d.cmdBuf
|
||||
_ = d.bus.Tx(nil, buf[:2])
|
||||
d.cs(true)
|
||||
return uint16(buf[1]) | uint16(buf[0])<<8
|
||||
}
|
||||
|
||||
func (d *Device) readByte(addr uint16, bsb uint8) byte {
|
||||
d.cs(false)
|
||||
d.sendReadHeader(addr, bsb)
|
||||
r, _ := d.bus.Transfer(byte(0))
|
||||
d.cs(true)
|
||||
return r
|
||||
}
|
||||
|
||||
func (d *Device) write(addr uint16, bsb uint8, p []byte) {
|
||||
d.cs(false)
|
||||
if len(p) == 0 {
|
||||
return
|
||||
}
|
||||
d.sendWriteHeader(addr, bsb)
|
||||
_ = d.bus.Tx(p, nil)
|
||||
d.cs(true)
|
||||
}
|
||||
|
||||
func (d *Device) writeUint16(addr uint16, bsb uint8, v uint16) {
|
||||
d.cs(false)
|
||||
d.sendWriteHeader(addr, bsb)
|
||||
buf := d.cmdBuf
|
||||
buf[0] = byte(v >> 8)
|
||||
buf[1] = byte(v & 0xff)
|
||||
_ = d.bus.Tx(buf[:2], nil)
|
||||
d.cs(true)
|
||||
}
|
||||
|
||||
func (d *Device) writeByte(addr uint16, bsb uint8, b byte) {
|
||||
d.cs(false)
|
||||
d.sendWriteHeader(addr, bsb)
|
||||
_, _ = d.bus.Transfer(b)
|
||||
d.cs(true)
|
||||
}
|
||||
|
||||
func (d *Device) sendReadHeader(addr uint16, bsb uint8) {
|
||||
buf := d.cmdBuf
|
||||
buf[0] = byte(addr >> 8)
|
||||
buf[1] = byte(addr & 0xff)
|
||||
buf[2] = bsb << 3
|
||||
_ = d.bus.Tx(buf[:], nil)
|
||||
}
|
||||
|
||||
func (d *Device) sendWriteHeader(addr uint16, bsb uint8) {
|
||||
buf := d.cmdBuf
|
||||
buf[0] = byte(addr >> 8)
|
||||
buf[1] = byte(addr & 0xff)
|
||||
buf[2] = bsb<<3 | 0b100
|
||||
_ = d.bus.Tx(buf[:], nil)
|
||||
}
|
||||
-445
@@ -1,445 +0,0 @@
|
||||
package w5500
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/netdev"
|
||||
)
|
||||
|
||||
type socket struct {
|
||||
sockn uint8
|
||||
protocol uint8
|
||||
port uint16
|
||||
inUse bool
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (s *socket) setProtocol(proto byte) *socket {
|
||||
s.protocol = proto
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *socket) setPort(port uint16) *socket {
|
||||
s.port = port
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *socket) setInUse(inUse bool) *socket {
|
||||
s.inUse = inUse
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *socket) setClosed(closed bool) *socket {
|
||||
s.closed = closed
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *socket) reset() {
|
||||
s.protocol = 0
|
||||
s.port = 0
|
||||
s.inUse = false
|
||||
s.closed = false
|
||||
}
|
||||
|
||||
// GetHostByName resolves the given host name to an IP address.
|
||||
func (d *Device) GetHostByName(name string) (netip.Addr, error) {
|
||||
d.mu.Lock()
|
||||
dns := d.dns
|
||||
d.mu.Unlock()
|
||||
|
||||
if dns == nil {
|
||||
return netip.Addr{}, netdev.ErrNotSupported
|
||||
}
|
||||
return dns(name)
|
||||
}
|
||||
|
||||
func (d *Device) Socket(domain int, stype int, protocol int) (int, error) {
|
||||
if domain != netdev.AF_INET {
|
||||
return -1, netdev.ErrFamilyNotSupported
|
||||
}
|
||||
switch {
|
||||
case stype == netdev.SOCK_STREAM && protocol == netdev.IPPROTO_TCP:
|
||||
case stype == netdev.SOCK_DGRAM && protocol == netdev.IPPROTO_UDP:
|
||||
default:
|
||||
return -1, errors.New("unsupported combination of socket type and protocol")
|
||||
}
|
||||
|
||||
var proto byte
|
||||
switch protocol {
|
||||
case netdev.IPPROTO_TCP:
|
||||
proto = 1 // TCP
|
||||
case netdev.IPPROTO_UDP:
|
||||
proto = 2 // UDP
|
||||
default:
|
||||
return -1, netdev.ErrNotSupported
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
sockfd, sock, err := d.nextSocket()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
d.openSocket(sock.sockn, proto)
|
||||
|
||||
sock.setProtocol(proto).setInUse(true)
|
||||
return sockfd, nil
|
||||
}
|
||||
|
||||
func (d *Device) openSocket(sockn uint8, proto byte) {
|
||||
d.writeByte(sockMode, sockAddr(sockn), proto&0x0F)
|
||||
}
|
||||
|
||||
func (d *Device) Bind(sockfd int, ip netip.AddrPort) error {
|
||||
// The IP address is irrelevant. The configured ip will always be used.
|
||||
port := ip.Port()
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
sock, err := d.socket(sockfd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = d.bindSocket(sock.sockn, port); err != nil {
|
||||
return errors.New("could not set socket port: " + err.Error())
|
||||
}
|
||||
|
||||
sock.setPort(port)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) bindSocket(sockn uint8, port uint16) error {
|
||||
d.writeUint16(sockSrcPort, sockAddr(sockn), port)
|
||||
d.socketSendCmd(sockn, sockCmdOpen)
|
||||
if d.sockStatus(sockn) == sockStatusClosed {
|
||||
return errors.New("socket is closed after binding")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSockOpt sets the socket option for the given socket file descriptor.
|
||||
// It is not supported by the W5500, so it always returns an error.
|
||||
func (d *Device) SetSockOpt(int, int, int, any) error {
|
||||
return netdev.ErrNotSupported
|
||||
}
|
||||
|
||||
// Connect establishes a connection to the specified host and port or ip and port.
|
||||
//
|
||||
// If the host is an empty string, it will use the provided ip address and port,
|
||||
// otherwise it will resolve the host name to an IP address.
|
||||
func (d *Device) Connect(sockfd int, host string, ip netip.AddrPort) error {
|
||||
destIP := ip.Addr()
|
||||
if host != "" {
|
||||
var err error
|
||||
destIP, err = d.GetHostByName(host)
|
||||
if err != nil {
|
||||
return errors.New("could not resolve host " + host + ":" + err.Error())
|
||||
}
|
||||
}
|
||||
if !destIP.IsValid() || !destIP.Is4() {
|
||||
return errors.New("invalid destination IP address: " + destIP.String())
|
||||
}
|
||||
port := ip.Port()
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
sock, err := d.socket(sockfd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.write(sockDestIP, sockAddr(sock.sockn), destIP.AsSlice())
|
||||
d.writeUint16(sockDestPort, sockAddr(sock.sockn), port)
|
||||
d.socketSendCmd(sock.sockn, sockCmdOpen)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Listen sets the socket to listen for incoming connections on the specified socket file descriptor.
|
||||
//
|
||||
// The backlog parameter is ignored, as the W5500 does not support it.
|
||||
func (d *Device) Listen(sockfd int, _ int) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
sock, err := d.socket(sockfd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sock.protocol != 1 { // Only TCP sockets can listen
|
||||
return errors.New("not a TCP socket")
|
||||
}
|
||||
|
||||
if err = d.listen(sock.sockn); err != nil {
|
||||
return errors.New("could not send listen command: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) listen(sockn uint8) error {
|
||||
state := d.sockStatus(sockn)
|
||||
if state != sockStatusInit {
|
||||
return errors.New("socket is not in the initial state")
|
||||
}
|
||||
d.socketSendCmd(sockn, sockCmdListen)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Accept waits for an incoming connection on the specified socket file descriptor.
|
||||
func (d *Device) Accept(sockfd int) (int, netip.AddrPort, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
lsock, err := d.socket(sockfd)
|
||||
if err != nil {
|
||||
return -1, netip.AddrPort{}, errors.New("could not get socket: " + err.Error())
|
||||
}
|
||||
|
||||
if err = d.waitForEstablished(lsock.sockn); err != nil {
|
||||
return -1, netip.AddrPort{}, err
|
||||
}
|
||||
|
||||
// Acquire a new socket for the listening connection.
|
||||
csockfd, csock, err := d.nextSocket()
|
||||
if err != nil {
|
||||
return -1, netip.AddrPort{}, err
|
||||
}
|
||||
// Swap the socket numbers of the client and listening sockets.
|
||||
lsock.sockn, csock.sockn = csock.sockn, lsock.sockn
|
||||
|
||||
// Rebind the listening socket to the local address and port and start listening.
|
||||
d.openSocket(lsock.sockn, lsock.protocol)
|
||||
if err = d.bindSocket(lsock.sockn, lsock.port); err != nil {
|
||||
return -1, netip.AddrPort{}, errors.New("could not bind listening socket: " + err.Error())
|
||||
}
|
||||
if err = d.listen(lsock.sockn); err != nil {
|
||||
return -1, netip.AddrPort{}, errors.New("could not set listening socket: " + err.Error())
|
||||
}
|
||||
|
||||
csock.setInUse(true)
|
||||
|
||||
remoteIP := d.remoteIP(csock.sockn)
|
||||
return csockfd, remoteIP, nil
|
||||
}
|
||||
|
||||
func (d *Device) waitForEstablished(sockn uint8) error {
|
||||
for {
|
||||
status := d.sockStatus(sockn)
|
||||
switch status {
|
||||
case sockStatusEstablished:
|
||||
return nil
|
||||
case sockStatusClosed:
|
||||
return net.ErrClosed
|
||||
case sockStatusCloseWait:
|
||||
// The server closed the connection, so we need to reset the socket
|
||||
// and set it to listen again.
|
||||
if err := d.listen(sockn); err != nil {
|
||||
return errors.New("could not set socket to listen: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
d.irqPoll(sockn, sockIntConnect|sockIntDisconnect, time.Time{})
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) remoteIP(sockn uint8) netip.AddrPort {
|
||||
var rip [4]byte
|
||||
d.read(sockDestIP, sockAddr(sockn), rip[:])
|
||||
|
||||
var rport [2]byte
|
||||
d.read(sockDestPort, sockAddr(sockn), rport[:])
|
||||
|
||||
return netip.AddrPortFrom(netip.AddrFrom4(rip), uint16(rport[0])<<8|uint16(rport[1]))
|
||||
}
|
||||
|
||||
// Send sends data to the socket with the given file descriptor.
|
||||
// It blocks until all data is sent or the deadline is reached.
|
||||
func (d *Device) Send(sockfd int, buf []byte, _ int, deadline time.Time) (int, error) {
|
||||
bufLen := len(buf)
|
||||
if bufLen <= d.maxSockSize {
|
||||
// Fast path for small buffers.
|
||||
return d.sendChunk(sockfd, buf, deadline)
|
||||
}
|
||||
|
||||
var n int
|
||||
for i := 0; i < bufLen; i += d.maxSockSize {
|
||||
end := i + d.maxSockSize
|
||||
if end > bufLen {
|
||||
end = bufLen
|
||||
}
|
||||
|
||||
sent, err := d.sendChunk(sockfd, buf[i:end], deadline)
|
||||
if err != nil {
|
||||
return n, errors.New("could not send chunk: " + err.Error())
|
||||
}
|
||||
n += sent
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (d *Device) sendChunk(sockfd int, buf []byte, deadline time.Time) (int, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
sock, err := d.socket(sockfd)
|
||||
if err != nil {
|
||||
return 0, errors.New("could not get socket: " + err.Error())
|
||||
}
|
||||
if sock.closed {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
|
||||
bufLen := uint16(len(buf))
|
||||
if err = d.waitForFreeBuffer(sock.sockn, bufLen, deadline); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
sendPtr := d.readUint16(sockTXWritePtr, sockAddr(sock.sockn))
|
||||
|
||||
d.write(sendPtr, sock.sockn<<2|0b10, buf)
|
||||
d.writeUint16(sockTXWritePtr, sockAddr(sock.sockn), sendPtr+bufLen)
|
||||
d.writeByte(sockCmd, sockAddr(sock.sockn), sockCmdSend)
|
||||
|
||||
irq := d.irqPoll(sock.sockn, sockIntSendOK|sockIntDisconnect|sockIntTimeout, deadline)
|
||||
switch {
|
||||
case irq == sockIntUnknown:
|
||||
return 0, os.ErrDeadlineExceeded
|
||||
case irq&sockIntDisconnect != 0:
|
||||
sock.setClosed(true)
|
||||
return 0, net.ErrClosed
|
||||
case irq&sockIntTimeout != 0:
|
||||
return 0, netdev.ErrTimeout
|
||||
default:
|
||||
return int(bufLen), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) waitForFreeBuffer(sockn uint8, len uint16, deadline time.Time) error {
|
||||
for {
|
||||
freeSize := d.readUint16(sockTXFreeSize, sockAddr(sockn))
|
||||
if freeSize >= len {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !deadline.IsZero() && time.Now().After(deadline) {
|
||||
return netdev.ErrTimeout
|
||||
}
|
||||
|
||||
status := d.sockStatus(sockn)
|
||||
switch status {
|
||||
case sockStatusEstablished, sockStatusCloseWait:
|
||||
default:
|
||||
return errors.New("socket is not in a valid state for sending data")
|
||||
}
|
||||
|
||||
d.mu.Unlock()
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
d.mu.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
// Recv reads data from the socket with the given file descriptor into the provided buffer.
|
||||
// It blocks until data is available or the deadline is reached.
|
||||
func (d *Device) Recv(sockfd int, buf []byte, _ int, deadline time.Time) (int, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
sock, err := d.socket(sockfd)
|
||||
if err != nil {
|
||||
return 0, errors.New("could not get socket: " + err.Error())
|
||||
}
|
||||
if sock.closed {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
|
||||
size, err := d.waitForData(sock, deadline)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
recvPtr := d.readUint16(sockRXReadPtr, sockAddr(sock.sockn))
|
||||
|
||||
buf = buf[:min(size, len(buf))]
|
||||
d.read(recvPtr, sock.sockn<<2|0b00011, buf)
|
||||
d.writeUint16(sockRXReadPtr, sockAddr(sock.sockn), recvPtr+uint16(len(buf)))
|
||||
d.socketSendCmd(sock.sockn, sockCmdRecv)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (d *Device) waitForData(sock *socket, deadline time.Time) (int, error) {
|
||||
for {
|
||||
recvdSize := d.readUint16(sockRXReceivedSize, sockAddr(sock.sockn))
|
||||
if recvdSize > 0 {
|
||||
return int(recvdSize), nil
|
||||
}
|
||||
|
||||
irq := d.irqPoll(sock.sockn, sockIntReceive|sockIntDisconnect, deadline)
|
||||
switch {
|
||||
case irq == sockIntUnknown:
|
||||
return 0, os.ErrDeadlineExceeded
|
||||
case irq&sockIntDisconnect != 0:
|
||||
sock.setClosed(true)
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the socket with the given file descriptor.
|
||||
func (d *Device) Close(sockfd int) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
sock, err := d.socket(sockfd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.socketSendCmd(sock.sockn, sockCmdClose)
|
||||
sock.reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) nextSocket() (int, *socket, error) {
|
||||
for i, sock := range d.sockets {
|
||||
if sock.inUse {
|
||||
continue
|
||||
}
|
||||
return i, sock, nil
|
||||
}
|
||||
return -1, nil, netdev.ErrNoMoreSockets
|
||||
}
|
||||
|
||||
func (d *Device) socket(sockfd int) (*socket, error) {
|
||||
if sockfd < 0 || sockfd >= len(d.sockets) {
|
||||
return nil, netdev.ErrInvalidSocketFd
|
||||
}
|
||||
return d.sockets[sockfd], nil
|
||||
}
|
||||
|
||||
func (d *Device) socketSendCmd(sockn uint8, cmd byte) {
|
||||
d.writeByte(sockCmd, sockAddr(sockn), cmd)
|
||||
for d.readByte(sockCmd, sockAddr(sockn)) != 0 {
|
||||
runtime.Gosched()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) sockStatus(sockn uint8) int {
|
||||
return int(d.readByte(sockStatus, sockAddr(sockn)))
|
||||
}
|
||||
|
||||
func sockAddr(sockn uint8) uint8 {
|
||||
return sockn<<2 | 0b0001
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package w5500
|
||||
|
||||
// Common Registers.
|
||||
const (
|
||||
regMode = 0x0000
|
||||
regGatewayAddr = 0x0001
|
||||
regSubnetMask = 0x0005
|
||||
regMAC = 0x0009
|
||||
regIPAddr = 0x000F
|
||||
regIntLevel = 0x0013
|
||||
regInt = 0x0015
|
||||
regIntMask = 0x0016
|
||||
regSockInt = 0x0017
|
||||
regSockIntMask = 0x0018
|
||||
regRetryTime = 0x0019
|
||||
regRetryN = 0x001B
|
||||
// ... PPP registers, not needed
|
||||
regPHYCfg = 0x002E
|
||||
regChipVer = 0x0039
|
||||
)
|
||||
|
||||
// Socket Registers.
|
||||
const (
|
||||
sockMode = 0x0000
|
||||
sockCmd = 0x0001
|
||||
sockInt = 0x0002
|
||||
sockStatus = 0x0003
|
||||
sockSrcPort = 0x0004
|
||||
sockDestMAC = 0x0006
|
||||
sockDestIP = 0x000C
|
||||
sockDestPort = 0x0010
|
||||
sockMaxSegSize = 0x0012
|
||||
sockIPTOS = 0x0015
|
||||
sockIPTTL = 0x0016
|
||||
sockRXBUFSize = 0x001E
|
||||
sockTXBUFSize = 0x001F
|
||||
sockTXFreeSize = 0x0020
|
||||
sockTXReadPtr = 0x0022
|
||||
sockTXWritePtr = 0x0024
|
||||
sockRXReceivedSize = 0x0026
|
||||
sockRXReadPtr = 0x0028
|
||||
sockRXWritePtr = 0x002A
|
||||
sockIntMask = 0x002C
|
||||
sockKeepInt = 0x002F
|
||||
)
|
||||
|
||||
// Socket Commands.
|
||||
const (
|
||||
sockCmdOpen = 0x01
|
||||
sockCmdClose = 0x10
|
||||
sockCmdListen = 0x02
|
||||
sockCmdConnect = 0x04
|
||||
sockCmdDisconnect = 0x08
|
||||
sockCmdSend = 0x20
|
||||
sockCmdSendMacRaw = 0x21
|
||||
sockCmdSendKeep = 0x22
|
||||
sockCmdRecv = 0x40
|
||||
)
|
||||
|
||||
// Socket Statuses.
|
||||
const (
|
||||
sockStatusClosed = 0x00
|
||||
sockStatusInit = 0x13
|
||||
sockStatusListen = 0x14
|
||||
sockStatusEstablished = 0x17
|
||||
sockStatusCloseWait = 0x1C
|
||||
sockStatusUdp = 0x22
|
||||
sockStatusMacRaw = 0x42
|
||||
// Temporary TCP states
|
||||
sockStatusSynSent = 0x15
|
||||
sockStatusSynRecv = 0x16
|
||||
sockStatusFinWait = 0x18
|
||||
sockStatusClosing = 0x1A
|
||||
sockStatusTimeWait = 0x1B
|
||||
sockStatusLastAck = 0x1D
|
||||
sockStatusUnknown = 0xFF
|
||||
)
|
||||
|
||||
// Socket Interrupts.
|
||||
const (
|
||||
sockIntConnect uint8 = 1 << iota
|
||||
sockIntDisconnect
|
||||
sockIntReceive
|
||||
sockIntTimeout
|
||||
sockIntSendOK
|
||||
|
||||
sockIntUnknown uint8 = 0
|
||||
)
|
||||
-248
@@ -1,248 +0,0 @@
|
||||
// Package w5500 implements a driver for the W5500 Ethernet controller.
|
||||
//
|
||||
// The driver supports basic network functionality including TCP and UDP sockets.
|
||||
// It currently does not use the IRQ or RST pins.
|
||||
//
|
||||
// Datasheet: https://docs.wiznet.io/img/products/w5500/W5500_ds_v110e.pdf
|
||||
// Product Page: https://wiznet.io/products/ethernet-chips/w5500
|
||||
package w5500
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
"tinygo.org/x/drivers/netdev"
|
||||
)
|
||||
|
||||
var _ netdev.Netdever = &Device{}
|
||||
|
||||
// Resolver is a function that resolves a hostname to an IP address.
|
||||
type Resolver func(host string) (netip.Addr, error)
|
||||
|
||||
// Device is a driver for the W5500 Ethernet controller.
|
||||
type Device struct {
|
||||
maxSockets int
|
||||
maxSockSize int
|
||||
|
||||
mu sync.Mutex
|
||||
bus drivers.SPI
|
||||
cs pin.OutputFunc
|
||||
dns Resolver
|
||||
|
||||
sockets []*socket
|
||||
laddr netip.Addr
|
||||
|
||||
cmdBuf [3]byte
|
||||
}
|
||||
|
||||
// New returns a new w5500 driver.
|
||||
func New(bus drivers.SPI, csPin pin.Output) *Device {
|
||||
return &Device{
|
||||
bus: bus,
|
||||
cs: csPin.Set,
|
||||
}
|
||||
}
|
||||
|
||||
// Config is the configuration for the device.
|
||||
//
|
||||
// The SPI bus must be fully configured.
|
||||
type Config struct {
|
||||
DNS Resolver
|
||||
|
||||
MAC net.HardwareAddr
|
||||
IP netip.Addr
|
||||
SubnetMask netip.Addr
|
||||
Gateway netip.Addr
|
||||
|
||||
// Optional, default is 8.
|
||||
MaxSockets int
|
||||
}
|
||||
|
||||
// Configure sets up the device.
|
||||
//
|
||||
// MAC address must be provided. The other fields are optional.
|
||||
func (d *Device) Configure(cfg Config) error {
|
||||
d.cs(true)
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.dns = cfg.DNS
|
||||
|
||||
d.reset()
|
||||
|
||||
if err := d.setupSockets(cfg.MaxSockets); err != nil {
|
||||
return errors.New("could not setup sockets: " + err.Error())
|
||||
}
|
||||
|
||||
// Set the MAC address and IP configuration.
|
||||
d.write(regMAC, 0, cfg.MAC)
|
||||
d.write(regIPAddr, 0, cfg.IP.AsSlice())
|
||||
d.write(regSubnetMask, 0, cfg.SubnetMask.AsSlice())
|
||||
d.write(regGatewayAddr, 0, cfg.Gateway.AsSlice())
|
||||
d.laddr = cfg.IP
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) setupSockets(maxSockets int) error {
|
||||
if maxSockets == 0 {
|
||||
maxSockets = 8 // Default to 8 sockets if not specified.
|
||||
}
|
||||
switch maxSockets {
|
||||
case 1, 2, 4, 8:
|
||||
// Valid socket counts.
|
||||
default:
|
||||
return errors.New("invalid number of sockets, must be one of 1, 2, 4, or 8")
|
||||
}
|
||||
|
||||
socks := make([]*socket, maxSockets)
|
||||
for i := range socks {
|
||||
socks[i] = &socket{
|
||||
sockn: uint8(i),
|
||||
}
|
||||
}
|
||||
|
||||
d.maxSockets = maxSockets
|
||||
d.maxSockSize = 16 * 1024 / maxSockets
|
||||
d.sockets = socks
|
||||
|
||||
// Set the RX and TX buffer sizes for each socket.
|
||||
for i := 0; i < 8; i++ {
|
||||
size := byte(d.maxSockSize >> 10)
|
||||
if i >= maxSockets {
|
||||
size = 0
|
||||
}
|
||||
|
||||
d.writeByte(sockRXBUFSize, sockAddr(uint8(i)), size)
|
||||
d.writeByte(sockTXBUFSize, sockAddr(uint8(i)), size)
|
||||
}
|
||||
|
||||
mask := byte(0b11111111)
|
||||
switch maxSockets {
|
||||
case 1:
|
||||
mask = 0b00000001
|
||||
case 2:
|
||||
mask = 0b00000011
|
||||
case 4:
|
||||
mask = 0b00001111
|
||||
}
|
||||
d.writeByte(regSockIntMask, 0, mask)
|
||||
d.writeByte(regIntMask, 0, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset performs a soft reset.
|
||||
func (d *Device) Reset() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *Device) reset() {
|
||||
// RST is bit 7 of regMode.
|
||||
d.writeByte(regMode, 0, 0x80)
|
||||
}
|
||||
|
||||
// GetHardwareAddr returns the hardware address of the device.
|
||||
func (d *Device) GetHardwareAddr() (net.HardwareAddr, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
mac := make([]byte, 6)
|
||||
d.read(regMAC, 0, mac)
|
||||
return mac, nil
|
||||
}
|
||||
|
||||
// Addr returns the IP address of the device.
|
||||
func (d *Device) Addr() (netip.Addr, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
var ip [4]byte
|
||||
d.read(regIPAddr, 0, ip[:])
|
||||
return netip.AddrFrom4(ip), nil
|
||||
}
|
||||
|
||||
// SetAddr sets the IP address of the device.
|
||||
//
|
||||
// The IP address must be a valid IPv4 address.
|
||||
func (d *Device) SetAddr(ip netip.Addr) error {
|
||||
if err := d.setAddress(regIPAddr, ip); err != nil {
|
||||
return errors.New("could not set IP address: " + err.Error())
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.laddr = ip
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSubnetMask sets the subnet mask of the device.
|
||||
//
|
||||
// The subnet mask must be a valid IPv4 address.
|
||||
// It is not checked if the subnet mask is valid for the device's IP address.
|
||||
func (d *Device) SetSubnetMask(mask netip.Addr) error {
|
||||
return d.setAddress(regSubnetMask, mask)
|
||||
}
|
||||
|
||||
// SetGateway sets the gateway address of the device.
|
||||
//
|
||||
// The gateway must be a valid IPv4 address.
|
||||
// It is not checked if the gateway is in the same subnet as the device.
|
||||
func (d *Device) SetGateway(gateway netip.Addr) error {
|
||||
return d.setAddress(regGatewayAddr, gateway)
|
||||
}
|
||||
|
||||
func (d *Device) setAddress(addr uint16, ip netip.Addr) error {
|
||||
if !ip.IsValid() || !ip.Is4() {
|
||||
return errors.New("invalid IP address: " + ip.String())
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.write(addr, 0, ip.AsSlice())
|
||||
return nil
|
||||
}
|
||||
|
||||
// LinkStatus is the link status of the device.
|
||||
type LinkStatus = uint8
|
||||
|
||||
// LinkStatus values.
|
||||
const (
|
||||
LinkStatusDown LinkStatus = iota
|
||||
LinkStatusUp
|
||||
)
|
||||
|
||||
// LinkStatus returns the current link status of the device.
|
||||
func (d *Device) LinkStatus() LinkStatus {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
return d.readByte(regPHYCfg, 0) & 0b00000001
|
||||
}
|
||||
|
||||
// LinkInfo returns the current link information of the device.
|
||||
func (d *Device) LinkInfo() string {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
linkInfo := d.readByte(regPHYCfg, 0) & 0b00000110
|
||||
|
||||
speed := "10Mbps"
|
||||
if linkInfo&0b00000010 != 0 {
|
||||
speed = "100Mbps"
|
||||
}
|
||||
duplex := "Half Duplex"
|
||||
if linkInfo&0b00000100 != 0 {
|
||||
duplex = "Full Duplex"
|
||||
}
|
||||
return speed + " " + duplex
|
||||
}
|
||||
@@ -1,465 +0,0 @@
|
||||
// Package epd2in9v2 implements a driver for the Waveshare 2.9in V2 black and white e-paper display.
|
||||
//
|
||||
// This is for the V2 device using the SSD1680 chipset. For the V1 device (using IL3820),
|
||||
// use the epd2in9 package instead.
|
||||
//
|
||||
// Datasheet:
|
||||
// https://files.waveshare.com/upload/7/79/2.9inch-e-paper-v2-specification.pdf
|
||||
// https://cdn-learn.adafruit.com/assets/assets/000/097/631/original/SSD1680_Datasheet.pdf?1607625960
|
||||
//
|
||||
// Reference: https://github.com/waveshareteam/e-Paper/tree/master/RaspberryPi_JetsonNano/c/lib/e-Paper
|
||||
package epd2in9v2 // import "tinygo.org/x/drivers/waveshare-epd/epd2in9v2"
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Width int16
|
||||
Height int16
|
||||
Rotation Rotation
|
||||
Speed Speed
|
||||
Blocking bool
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
bus drivers.SPI
|
||||
cs machine.Pin
|
||||
dc machine.Pin
|
||||
rst machine.Pin
|
||||
busy machine.Pin
|
||||
width int16
|
||||
height int16
|
||||
buffer []uint8
|
||||
bufferLength uint32
|
||||
rotation Rotation
|
||||
speed Speed
|
||||
blocking bool
|
||||
}
|
||||
|
||||
type Rotation uint8
|
||||
type Speed uint8
|
||||
|
||||
// LUT for normal full refresh (~2s)
|
||||
var lutDefault = [159]uint8{
|
||||
0x80, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0,
|
||||
0x10, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0,
|
||||
0x80, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0,
|
||||
0x10, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x14, 0x8, 0x0, 0x0, 0x0, 0x0, 0x1,
|
||||
0xA, 0xA, 0x0, 0xA, 0xA, 0x0, 0x1,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x14, 0x8, 0x0, 0x1, 0x0, 0x0, 0x1,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x0, 0x0, 0x0,
|
||||
0x22, 0x17, 0x41, 0x0, 0x32, 0x36,
|
||||
}
|
||||
|
||||
// LUT for fast full refresh (~1s)
|
||||
var lutFast = [159]uint8{
|
||||
0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x19, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x24, 0x42, 0x22, 0x22, 0x23, 0x32, 0x00, 0x00, 0x00,
|
||||
0x22, 0x17, 0x41, 0xAE, 0x32, 0x38,
|
||||
}
|
||||
|
||||
// LUT for partial refresh
|
||||
var lutPartial = [159]uint8{
|
||||
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2,
|
||||
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0,
|
||||
0x22, 0x17, 0x41, 0xB0, 0x32, 0x36,
|
||||
}
|
||||
|
||||
// New returns a new epd2in9v2 driver. Pass in a fully configured SPI bus.
|
||||
func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device {
|
||||
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
busyPin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
return Device{
|
||||
bus: bus,
|
||||
cs: csPin,
|
||||
dc: dcPin,
|
||||
rst: rstPin,
|
||||
busy: busyPin,
|
||||
}
|
||||
}
|
||||
|
||||
// Configure sets up the device.
|
||||
func (d *Device) Configure(cfg Config) {
|
||||
if cfg.Width != 0 {
|
||||
d.width = cfg.Width
|
||||
} else {
|
||||
d.width = EPD_WIDTH
|
||||
}
|
||||
if cfg.Height != 0 {
|
||||
d.height = cfg.Height
|
||||
} else {
|
||||
d.height = EPD_HEIGHT
|
||||
}
|
||||
d.rotation = cfg.Rotation
|
||||
d.speed = cfg.Speed
|
||||
d.blocking = cfg.Blocking
|
||||
d.bufferLength = (uint32(d.width) * uint32(d.height)) / 8
|
||||
d.buffer = make([]uint8, d.bufferLength)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.buffer[i] = 0xFF
|
||||
}
|
||||
|
||||
d.Reset()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
d.WaitUntilIdle()
|
||||
d.SendCommand(SW_RESET)
|
||||
d.WaitUntilIdle()
|
||||
|
||||
d.SendCommand(DRIVER_OUTPUT_CONTROL)
|
||||
d.SendData(uint8((d.height - 1) & 0xFF))
|
||||
d.SendData(uint8((d.height - 1) >> 8))
|
||||
d.SendData(0x00)
|
||||
|
||||
d.SendCommand(DATA_ENTRY_MODE)
|
||||
d.SendData(0x03)
|
||||
|
||||
d.setWindow(0, 0, d.width-1, d.height-1)
|
||||
|
||||
if cfg.Speed == SPEED_FAST {
|
||||
d.SendCommand(BORDER_WAVEFORM_CONTROL)
|
||||
d.SendData(0x05)
|
||||
}
|
||||
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_1)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x80)
|
||||
|
||||
d.setCursor(0, 0)
|
||||
d.WaitUntilIdle()
|
||||
|
||||
switch cfg.Speed {
|
||||
case SPEED_FAST:
|
||||
d.setLUTByHost(&lutFast)
|
||||
default:
|
||||
d.setLUTByHost(&lutDefault)
|
||||
}
|
||||
}
|
||||
|
||||
// HardwareReset resets the device via the RST pin.
|
||||
func (d *Device) Reset() {
|
||||
d.rst.High()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.rst.Low()
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
d.rst.High()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// SendCommand sends a command byte to the display.
|
||||
func (d *Device) SendCommand(command uint8) {
|
||||
d.dc.Low()
|
||||
d.cs.Low()
|
||||
d.bus.Transfer(command)
|
||||
d.cs.High()
|
||||
}
|
||||
|
||||
// SendData sends a data byte to the display.
|
||||
func (d *Device) SendData(data uint8) {
|
||||
d.dc.High()
|
||||
d.cs.Low()
|
||||
d.bus.Transfer(data)
|
||||
d.cs.High()
|
||||
}
|
||||
|
||||
// WaitUntilIdle waits until the display is ready.
|
||||
// On SSD1680, BUSY pin is HIGH when busy, LOW when idle.
|
||||
func (d *Device) WaitUntilIdle() {
|
||||
for d.busy.Get() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
// IsBusy returns the busy status of the display.
|
||||
func (d *Device) IsBusy() bool {
|
||||
return d.busy.Get()
|
||||
}
|
||||
|
||||
// SetPixel modifies the internal buffer in a single pixel.
|
||||
// Uses color.RGBA where black (0,0,0) = black pixel, anything else = white pixel.
|
||||
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
|
||||
x, y = d.xy(x, y)
|
||||
|
||||
if x < 0 || x >= d.width || y < 0 || y >= d.height {
|
||||
return
|
||||
}
|
||||
byteIndex := (y * (d.width / 8)) + (x / 8)
|
||||
if c.R == 0 && c.G == 0 && c.B == 0 {
|
||||
d.buffer[byteIndex] &^= 0x80 >> uint8(x%8)
|
||||
} else {
|
||||
d.buffer[byteIndex] |= 0x80 >> uint8(x%8)
|
||||
}
|
||||
}
|
||||
|
||||
// Display sends the buffer to the screen.
|
||||
func (d *Device) Display() error {
|
||||
if d.blocking {
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
d.setCursor(0, 0)
|
||||
d.SendCommand(WRITE_RAM_BW)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.SendData(d.buffer[i])
|
||||
}
|
||||
|
||||
d.turnOnDisplay()
|
||||
|
||||
if d.blocking {
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisplayWithBase writes the buffer to both BW and RED RAM then refreshes.
|
||||
// This is useful before partial updates to set the base image.
|
||||
func (d *Device) DisplayWithBase() error {
|
||||
if d.blocking {
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
d.setCursor(0, 0)
|
||||
d.SendCommand(WRITE_RAM_BW)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.SendData(d.buffer[i])
|
||||
}
|
||||
|
||||
d.setCursor(0, 0)
|
||||
d.SendCommand(WRITE_RAM_RED)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.SendData(d.buffer[i])
|
||||
}
|
||||
|
||||
d.turnOnDisplay()
|
||||
|
||||
if d.blocking {
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisplayPartial performs a partial refresh of the display.
|
||||
// Call DisplayWithBase first to set the base image before using partial updates.
|
||||
func (d *Device) DisplayPartial() error {
|
||||
d.rst.Low()
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
d.rst.High()
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
d.setLUT(&lutPartial)
|
||||
|
||||
d.SendCommand(OTP_SELECTION_CONTROL)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x40)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
|
||||
d.SendCommand(BORDER_WAVEFORM_CONTROL)
|
||||
d.SendData(0x80)
|
||||
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_2)
|
||||
d.SendData(0xC0)
|
||||
d.SendCommand(MASTER_ACTIVATION)
|
||||
d.WaitUntilIdle()
|
||||
|
||||
d.setWindow(0, 0, d.width-1, d.height-1)
|
||||
d.setCursor(0, 0)
|
||||
|
||||
d.SendCommand(WRITE_RAM_BW)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.SendData(d.buffer[i])
|
||||
}
|
||||
|
||||
d.turnOnDisplayPartial()
|
||||
d.WaitUntilIdle()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearDisplay erases the display.
|
||||
func (d *Device) ClearDisplay() {
|
||||
d.ClearBuffer()
|
||||
d.Display()
|
||||
}
|
||||
|
||||
// ClearBuffer sets the buffer to 0xFF (white).
|
||||
func (d *Device) ClearBuffer() {
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.buffer[i] = 0xFF
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the current size of the display.
|
||||
func (d *Device) Size() (w, h int16) {
|
||||
if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 {
|
||||
return d.height, d.width
|
||||
}
|
||||
return d.width, d.height
|
||||
}
|
||||
|
||||
// SetRotation changes the rotation (clock-wise) of the device.
|
||||
func (d *Device) SetRotation(rotation Rotation) {
|
||||
d.rotation = rotation
|
||||
}
|
||||
|
||||
// SetBlocking changes the blocking flag of the device.
|
||||
func (d *Device) SetBlocking(blocking bool) {
|
||||
d.blocking = blocking
|
||||
}
|
||||
|
||||
// SetSpeed changes the refresh speed and reconfigures the device.
|
||||
func (d *Device) SetSpeed(speed Speed) {
|
||||
d.Configure(Config{
|
||||
Width: d.width,
|
||||
Height: d.height,
|
||||
Rotation: d.rotation,
|
||||
Speed: speed,
|
||||
Blocking: d.blocking,
|
||||
})
|
||||
}
|
||||
|
||||
// Sleep puts the display into deep sleep mode. A hardware reset is needed to wake it.
|
||||
func (d *Device) Sleep() {
|
||||
d.SendCommand(DEEP_SLEEP_MODE)
|
||||
d.SendData(0x01)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// PowerOff disables the display analog/clock. Lighter than Sleep.
|
||||
func (d *Device) PowerOff() {
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_2)
|
||||
d.SendData(0x03)
|
||||
d.SendCommand(MASTER_ACTIVATION)
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
func (d *Device) xy(x, y int16) (int16, int16) {
|
||||
switch d.rotation {
|
||||
case NO_ROTATION:
|
||||
return x, y
|
||||
case ROTATION_90:
|
||||
return d.width - y - 1, x
|
||||
case ROTATION_180:
|
||||
return d.width - x - 1, d.height - y - 1
|
||||
case ROTATION_270:
|
||||
return y, d.height - x - 1
|
||||
}
|
||||
return x, y
|
||||
}
|
||||
|
||||
func (d *Device) setWindow(xStart, yStart, xEnd, yEnd int16) {
|
||||
d.SendCommand(SET_RAM_X_ADDRESS)
|
||||
d.SendData(uint8((xStart >> 3) & 0xFF))
|
||||
d.SendData(uint8((xEnd >> 3) & 0xFF))
|
||||
|
||||
d.SendCommand(SET_RAM_Y_ADDRESS)
|
||||
d.SendData(uint8(yStart & 0xFF))
|
||||
d.SendData(uint8((yStart >> 8) & 0xFF))
|
||||
d.SendData(uint8(yEnd & 0xFF))
|
||||
d.SendData(uint8((yEnd >> 8) & 0xFF))
|
||||
}
|
||||
|
||||
func (d *Device) setCursor(x, y int16) {
|
||||
d.SendCommand(SET_RAM_X_COUNTER)
|
||||
d.SendData(uint8(x & 0xFF))
|
||||
|
||||
d.SendCommand(SET_RAM_Y_COUNTER)
|
||||
d.SendData(uint8(y & 0xFF))
|
||||
d.SendData(uint8((y >> 8) & 0xFF))
|
||||
}
|
||||
|
||||
func (d *Device) turnOnDisplay() {
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_2)
|
||||
d.SendData(0xC7)
|
||||
d.SendCommand(MASTER_ACTIVATION)
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
func (d *Device) turnOnDisplayPartial() {
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_2)
|
||||
d.SendData(0x0F)
|
||||
d.SendCommand(MASTER_ACTIVATION)
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
func (d *Device) setLUT(lut *[159]uint8) {
|
||||
d.SendCommand(WRITE_LUT_REGISTER)
|
||||
for i := 0; i < 153; i++ {
|
||||
d.SendData(lut[i])
|
||||
}
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
func (d *Device) setLUTByHost(lut *[159]uint8) {
|
||||
d.setLUT(lut)
|
||||
|
||||
d.SendCommand(END_OPTION)
|
||||
d.SendData(lut[153])
|
||||
|
||||
d.SendCommand(GATE_DRIVING_VOLTAGE)
|
||||
d.SendData(lut[154])
|
||||
|
||||
d.SendCommand(SOURCE_DRIVING_VOLTAGE)
|
||||
d.SendData(lut[155])
|
||||
d.SendData(lut[156])
|
||||
d.SendData(lut[157])
|
||||
|
||||
d.SendCommand(WRITE_VCOM_REGISTER)
|
||||
d.SendData(lut[158])
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package epd2in9v2
|
||||
|
||||
// Commands from SSD1680 datasheet
|
||||
const (
|
||||
EPD_WIDTH = 128
|
||||
EPD_HEIGHT = 296
|
||||
|
||||
DRIVER_OUTPUT_CONTROL = 0x01
|
||||
GATE_DRIVING_VOLTAGE = 0x03
|
||||
SOURCE_DRIVING_VOLTAGE = 0x04
|
||||
DEEP_SLEEP_MODE = 0x10
|
||||
DATA_ENTRY_MODE = 0x11
|
||||
SW_RESET = 0x12
|
||||
MASTER_ACTIVATION = 0x20
|
||||
DISPLAY_UPDATE_CONTROL_1 = 0x21
|
||||
DISPLAY_UPDATE_CONTROL_2 = 0x22
|
||||
WRITE_RAM_BW = 0x24
|
||||
WRITE_RAM_RED = 0x26
|
||||
VCOM_SENSE = 0x28
|
||||
VCOM_SENSE_DURATION = 0x29
|
||||
PROGRAM_VCOM_OTP = 0x2A
|
||||
WRITE_VCOM_CONTROL = 0x2B
|
||||
WRITE_VCOM_REGISTER = 0x2C
|
||||
OTP_READ_DISPLAY_OPTION = 0x2D
|
||||
USER_ID_READ = 0x2E
|
||||
PROGRAM_WS_OTP = 0x30
|
||||
LOAD_WS_OTP = 0x31
|
||||
WRITE_LUT_REGISTER = 0x32
|
||||
PROGRAM_OTP_SELECTION = 0x36
|
||||
OTP_SELECTION_CONTROL = 0x37
|
||||
WRITE_USER_ID = 0x38
|
||||
OTP_PROGRAM_MODE = 0x39
|
||||
BORDER_WAVEFORM_CONTROL = 0x3C
|
||||
END_OPTION = 0x3F
|
||||
SET_RAM_X_ADDRESS = 0x44
|
||||
SET_RAM_Y_ADDRESS = 0x45
|
||||
SET_RAM_X_COUNTER = 0x4E
|
||||
SET_RAM_Y_COUNTER = 0x4F
|
||||
SET_ANALOG_BLOCK_CONTROL = 0x74
|
||||
SET_DIGITAL_BLOCK_CONTROL = 0x7E
|
||||
|
||||
NO_ROTATION Rotation = 0
|
||||
ROTATION_90 Rotation = 1
|
||||
ROTATION_180 Rotation = 2
|
||||
ROTATION_270 Rotation = 3
|
||||
|
||||
SPEED_DEFAULT Speed = 0
|
||||
SPEED_FAST Speed = 1
|
||||
SPEED_PARTIAL Speed = 2
|
||||
)
|
||||
+22
-41
@@ -1,7 +1,4 @@
|
||||
// Package ws2812 implements a driver for WS2812 and SK6812 RGB LED strips.
|
||||
//
|
||||
// On most platforms NewWS2812 uses bit-banging.
|
||||
// On RP2040/RP2350 it uses PIO for hardware-timed control.
|
||||
package ws2812 // import "tinygo.org/x/drivers/ws2812"
|
||||
|
||||
//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 168 200
|
||||
@@ -18,8 +15,7 @@ var errUnknownClockSpeed = errors.New("ws2812: unknown CPU clock speed")
|
||||
// Device wraps a pin object for an easy driver interface.
|
||||
type Device struct {
|
||||
Pin machine.Pin
|
||||
brightness uint8
|
||||
writeColorFunc func(Device, []color.RGBA, uint8) error
|
||||
writeColorFunc func(Device, []color.RGBA) error
|
||||
}
|
||||
|
||||
// deprecated, use NewWS2812 or NewSK6812 depending on which device you want.
|
||||
@@ -28,28 +24,24 @@ func New(pin machine.Pin) Device {
|
||||
return NewWS2812(pin)
|
||||
}
|
||||
|
||||
// NewWS2812 returns a new WS2812(RGB) driver.
|
||||
// On RP2040/RP2350, it uses PIO for hardware-timed control.
|
||||
// On other platforms, you must configure the pin as output before calling this.
|
||||
// New returns a new WS2812(RGB) driver.
|
||||
// It does not touch the pin object: you have
|
||||
// to configure it as an output pin before calling New.
|
||||
func NewWS2812(pin machine.Pin) Device {
|
||||
return newWS2812Device(pin)
|
||||
}
|
||||
|
||||
// NewSK6812 returns a new SK6812W/RGBW driver (4 channels, GRBW order, 32-bit protocol), for the 3 channels version use NewWS2812
|
||||
// Use this for SK6812W strips that have a dedicated white channel controlled via color.A.
|
||||
// It does not touch the pin object: you have to configure it as an output pin before
|
||||
// calling this.
|
||||
func NewSK6812(pin machine.Pin) Device {
|
||||
return Device{
|
||||
Pin: pin,
|
||||
brightness: 255,
|
||||
writeColorFunc: writeColorsRGBA,
|
||||
writeColorFunc: writeColorsRGB,
|
||||
}
|
||||
}
|
||||
|
||||
// SetBrightness sets the global brightness (0-255).
|
||||
func (d *Device) SetBrightness(b uint8) {
|
||||
d.brightness = b
|
||||
// New returns a new SK6812(RGBA) driver.
|
||||
// It does not touch the pin object: you have
|
||||
// to configure it as an output pin before calling New.
|
||||
func NewSK6812(pin machine.Pin) Device {
|
||||
return Device{
|
||||
Pin: pin,
|
||||
writeColorFunc: writeColorsRGBA,
|
||||
}
|
||||
}
|
||||
|
||||
// Write the raw bitstring out using the WS2812 protocol.
|
||||
@@ -63,35 +55,24 @@ func (d Device) Write(buf []byte) (n int, err error) {
|
||||
// Write the given color slice out using the WS2812 protocol.
|
||||
// Colors are sent out in the usual GRB(A) format.
|
||||
func (d Device) WriteColors(buf []color.RGBA) (err error) {
|
||||
return d.writeColorFunc(d, buf, d.brightness)
|
||||
return d.writeColorFunc(d, buf)
|
||||
}
|
||||
|
||||
func writeColorsRGB(d Device, buf []color.RGBA, brightness uint8) (err error) {
|
||||
func writeColorsRGB(d Device, buf []color.RGBA) (err error) {
|
||||
for _, color := range buf {
|
||||
r, g, b := applyBrightness(color, brightness)
|
||||
d.WriteByte(g) // green
|
||||
d.WriteByte(r) // red
|
||||
err = d.WriteByte(b) // blue
|
||||
d.WriteByte(color.G) // green
|
||||
d.WriteByte(color.R) // red
|
||||
err = d.WriteByte(color.B) // blue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeColorsRGBA(d Device, buf []color.RGBA, brightness uint8) (err error) {
|
||||
func writeColorsRGBA(d Device, buf []color.RGBA) (err error) {
|
||||
for _, color := range buf {
|
||||
r, g, b := applyBrightness(color, brightness)
|
||||
|
||||
d.WriteByte(g) // green
|
||||
d.WriteByte(r) // red
|
||||
d.WriteByte(b) // blue
|
||||
d.WriteByte(color.G) // green
|
||||
d.WriteByte(color.R) // red
|
||||
d.WriteByte(color.B) // blue
|
||||
err = d.WriteByte(color.A) // alpha
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// applyBrightness scales a color by the brightness value.
|
||||
func applyBrightness(c color.RGBA, brightness uint8) (r, g, b uint8) {
|
||||
r = uint8((uint16(c.R) * uint16(brightness)) >> 8)
|
||||
g = uint8((uint16(c.G) * uint16(brightness)) >> 8)
|
||||
b = uint8((uint16(c.B) * uint16(brightness)) >> 8)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
//go:build !rp2040 && !rp2350
|
||||
|
||||
package ws2812
|
||||
|
||||
import "machine"
|
||||
|
||||
// newWS2812Device creates a WS2812 device using the bit-bang driver.
|
||||
func newWS2812Device(pin machine.Pin) Device {
|
||||
return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
//go:build rp2040 || rp2350
|
||||
|
||||
package ws2812
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
|
||||
pio "github.com/tinygo-org/pio/rp2-pio"
|
||||
"github.com/tinygo-org/pio/rp2-pio/piolib"
|
||||
)
|
||||
|
||||
// newWS2812Device creates a WS2812 device using PIO for hardware-timed control.
|
||||
// If PIO initialization fails, it falls back to the bit-bang driver.
|
||||
func newWS2812Device(pin machine.Pin) Device {
|
||||
sm, err := pio.PIO0.ClaimStateMachine()
|
||||
if err != nil {
|
||||
return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB}
|
||||
}
|
||||
ws, err := piolib.NewWS2812B(sm, pin)
|
||||
if err != nil {
|
||||
return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB}
|
||||
}
|
||||
return Device{
|
||||
Pin: pin,
|
||||
brightness: 255,
|
||||
writeColorFunc: func(_ Device, buf []color.RGBA, brightness uint8) error {
|
||||
for _, c := range buf {
|
||||
r, g, b := applyBrightness(c, brightness)
|
||||
ws.PutRGB(r, g, b)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user