diff --git a/pkg/jiecang/common.go b/pkg/jiecang/common.go index 0e06f35..1cc7055 100644 --- a/pkg/jiecang/common.go +++ b/pkg/jiecang/common.go @@ -3,8 +3,21 @@ package jiecang // Common functions used to decode messages from/to the controller // checking validity etc. -// Function that checks whether data received from DataIn are valid. -// They should start with "f2f2", end with "7e" and te previous to last byte (which is a checksum) should not fail. +// isValidData validates data received from the controller's DataOut characteristic. +// +// The Jiecang protocol uses the following message format: +// - Bytes 0-1: Preamble (0xf2, 0xf2) +// - Byte 2: Message type/command +// - Byte 3: Data length (number of data bytes) +// - Bytes 4..(4+dataLen-1): Data payload +// - Byte (len-2): Checksum (sum of type, length, and data bytes, mod 256) +// - Byte (len-1): Terminator (0x7e) +// +// Parameters: +// - buf: Raw response buffer from the controller +// +// Returns true if the message has valid preamble, terminator, and checksum; +// false otherwise. func isValidData(buf []byte) bool { // Check length first to prevent index out of bounds if len(buf) < 6 { diff --git a/pkg/jiecang/height.go b/pkg/jiecang/height.go index 09cab16..a506604 100644 --- a/pkg/jiecang/height.go +++ b/pkg/jiecang/height.go @@ -7,20 +7,41 @@ import ( "time" ) -// Contains functions for height movement only. +// This file contains functions for controlling desk height. -// Moves the desk up +// Up sends a command to move the desk upward by one increment. +// Equivalent to pressing the up button on the desk control panel once. +// Returns an error if the command transmission fails. func (j *Jiecang) Up() error { return j.sendCommand(commands["up"]) } -// Moves the desk down +// Down sends a command to move the desk downward by one increment. +// Equivalent to pressing the down button on the desk control panel once. +// Returns an error if the command transmission fails. func (j *Jiecang) Down() error { return j.sendCommand(commands["down"]) } -// Moves the desk to the designated height. Height should be within the limits -// of the desk. The operation can be cancelled via the provided context. +// GoToHeight moves the desk to the specified height in centimeters. +// +// The function validates that the target height is within the desk's configured +// limits (LowestHeight and HighestHeight), then sends movement commands and polls +// the current height until the target is reached or the context is cancelled. +// +// Parameters: +// - ctx: Context for timeout and cancellation. The operation can be interrupted +// by cancelling the context (e.g., with Ctrl+C or timeout). +// - height: Target height in centimeters. Must be between LowestHeight and +// HighestHeight (typically 60-120cm). +// +// Returns an error if: +// - The target height is out of range +// - Command transmission fails +// - The context is cancelled (returns ctx.Err()) +// +// The function polls the height every 200ms and sends a stop command when +// the target is reached or the operation is cancelled. func (j *Jiecang) GoToHeight(ctx context.Context, height uint8) error { //Ensure that height is within low and high limits of the desk. if height > j.HighestHeight || height < j.LowestHeight { @@ -68,41 +89,45 @@ func (j *Jiecang) GoToHeight(ctx context.Context, height uint8) error { return nil } +// FetchHeight requests the desk's saved memory preset heights from the controller. +// The command is sent twice as required by the protocol for reliability. +// +// The response contains the height values for all memory presets (1-4). +// The values are processed asynchronously by the characteristicReceiver callback +// and stored in the presets map. +// +// Returns an error if the command transmission fails. func (j *Jiecang) FetchHeight() error { - //Implements fetch_height command - - // Returns - /* - f2f2 25 02 044e 79 7e //044e =1100 in decimal. Memory 1 - f2f2 25 02 044e 79 7e - f2f2 26 02 030c 37 7e //030c = 780 in dec. Memory 2 - f2f2 26 02 030c 37 7e - f2f2 27 02 0372 9e 7e //0372 = 882 in dec. Memory 3 - f2f2 27 02 0372 9e 7e - f2f2 28 02 0000 2a 7e // Memory 4? - f2f2 28 02 0000 2a 7e - - */ if err := j.sendCommand(commands["fetch_height"]); err != nil { return err } return j.sendCommand(commands["fetch_height"]) } +// FetchHeightRange requests the desk's minimum and maximum height limits. +// The command is sent twice as required by the protocol for reliability. +// +// The response contains the highest and lowest height values that the desk +// can physically reach. The values are processed asynchronously by the +// characteristicReceiver callback and stored in HighestHeight and LowestHeight. +// +// Returns an error if the command transmission fails. func (j *Jiecang) FetchHeightRange() error { - // Implements fetch_height_range command - - //Retuns - /* - f2f2 07 04 04f8 026c 75 7e - LEN HGH LOW CSUM - */ if err := j.sendCommand(commands["fetch_height_range"]); err != nil { return err } return j.sendCommand(commands["fetch_height_range"]) } +// readHeight decodes the current height value from the controller response. +// The function extracts the height from bytes 4-5 of the response buffer and converts +// it from millimeters (protocol format) to centimeters (application format). +// +// Parameters: +// - buf: Response buffer from the controller. Expected format: +// [0xf2, 0xf2, type, 0x03, highByte, lowByte, ..., checksum, 0x7e] +// +// Returns the current height in centimeters, or 0 if the response type is invalid. func readHeight(buf []byte) uint8 { if buf[3] == 0x03 { height := int(buf[4])*256 + int(buf[5]) @@ -112,7 +137,19 @@ func readHeight(buf []byte) uint8 { return 0 } -// Handles the response of FetchHeightRange command from the controller +// readHeightRange decodes the height range limits from the controller response. +// This handles the response from the FetchHeightRange command, extracting both +// the maximum and minimum height limits that the desk can physically reach. +// +// Parameters: +// - buf: Response buffer from the controller. Expected format: +// [0xf2, 0xf2, type, 0x04, highestHighByte, highestLowByte, +// lowestHighByte, lowestLowByte, ..., checksum, 0x7e] +// +// Returns: +// - highestHeight: Maximum reachable height in centimeters +// - lowestHeight: Minimum reachable height in centimeters +// - (0, 0) if the response type is invalid func readHeightRange(buf []byte) (uint8, uint8) { if buf[3] == 0x04 { highestHeight := int(buf[4])*256 + int(buf[5]) diff --git a/pkg/jiecang/jiecang.go b/pkg/jiecang/jiecang.go index 8db4277..3eecbbd 100644 --- a/pkg/jiecang/jiecang.go +++ b/pkg/jiecang/jiecang.go @@ -1,3 +1,29 @@ +// Package jiecang provides control functionality for Jiecang standing desk controllers +// via Bluetooth Low Energy (BLE) communication using the Lierda LSD4BT-E95ASTD001 module. +// +// The package implements the Jiecang UART protocol over BLE, supporting: +// - Height control (up/down, go to specific height) +// - Memory presets (save and recall positions) +// - Height range queries +// - Desk settings (memory mode, anti-collision sensitivity) +// +// Example usage: +// +// ctx := context.Background() +// adapter := bluetooth.DefaultAdapter +// adapter.Enable() +// +// address := bluetooth.MustParseMAC("AA:BB:CC:DD:EE:FF") +// desk, err := jiecang.Init(adapter, bluetooth.Address{MACAddress: bluetooth.MACAddress{MAC: address}}) +// if err != nil { +// log.Fatal(err) +// } +// defer desk.Disconnect() +// +// // Move desk to 100cm height with timeout +// ctx, cancel := context.WithTimeout(ctx, 60*time.Second) +// defer cancel() +// desk.GoToHeight(ctx, 100) package jiecang import ( @@ -31,29 +57,61 @@ const ( BLECharDataOutId = 0xFE62 ) +// Jiecang represents a connection to a Jiecang desk controller. +// It manages BLE communication and maintains the current state of the desk. +// +// The struct uses a mutex to protect concurrent access to shared state, +// allowing safe use from multiple goroutines. Height values are stored +// in centimeters for convenience. type Jiecang struct { - device bluetooth.Device - dataIn bluetooth.DeviceCharacteristic - dataOut bluetooth.DeviceCharacteristic + device bluetooth.Device // BLE device connection + dataIn bluetooth.DeviceCharacteristic // Write characteristic for sending commands + dataOut bluetooth.DeviceCharacteristic // Read characteristic for receiving responses - //currentHeight in cm - currentHeight uint8 - mu sync.RWMutex + currentHeight uint8 // Current height in centimeters + mu sync.RWMutex // Protects concurrent access to shared state - //Memory presets (memory 1-4) - presets map[string]uint8 + presets map[string]uint8 // Memory presets (memory1-4) in centimeters - //Highest and lowest height of desk - LowestHeight uint8 + // LowestHeight is the minimum height limit of the desk in centimeters. + // Set during initialization from the controller. + LowestHeight uint8 + + // HighestHeight is the maximum height limit of the desk in centimeters. + // Set during initialization from the controller. HighestHeight uint8 - //Desk settings - // Memory mode (One-touch mode vs constant touch) + // MemoryConstantTouchMode indicates if memory mode requires constant touch. + // false = one-touch mode, true = constant touch mode. MemoryConstantTouchMode bool - // Anti-collision sensitivity (1 High, 2 Medium, 3 Low) - AntiCollisionSensitivity uint8 //TODO: Use iota + + // AntiCollisionSensitivity indicates the anti-collision sensitivity level. + // Valid values: 1 = High, 2 = Medium, 3 = Low + AntiCollisionSensitivity uint8 } +// Init initializes a connection to a Jiecang desk controller via Bluetooth. +// +// The function performs the following steps: +// 1. Connects to the BLE device at the specified address +// 2. Discovers the Jiecang service (0xFE60) +// 3. Discovers data input/output characteristics (0xFE61, 0xFE62) +// 4. Enables notifications for receiving responses +// 5. Queries the desk for current height, height range, and memory presets +// +// Returns an error if any step fails (connection, service discovery, +// characteristic discovery, or initial queries). +// +// Example: +// +// adapter := bluetooth.DefaultAdapter +// adapter.Enable() +// address := bluetooth.MustParseMAC("AA:BB:CC:DD:EE:FF") +// desk, err := jiecang.Init(adapter, bluetooth.Address{MACAddress: bluetooth.MACAddress{MAC: address}}) +// if err != nil { +// log.Fatal(err) +// } +// defer desk.Disconnect() func Init(a *bluetooth.Adapter, addr bluetooth.Address) (*Jiecang, error) { j := new(Jiecang) @@ -138,6 +196,9 @@ func Init(a *bluetooth.Adapter, addr bluetooth.Address) (*Jiecang, error) { return j, nil } +// Disconnect closes the BLE connection to the desk controller. +// Should be called when done using the controller to free resources. +// Safe to call even if the connection is already closed. func (j *Jiecang) Disconnect() error { return j.device.Disconnect() } @@ -151,16 +212,20 @@ func (j *Jiecang) sendCommand(buf []byte) error { return nil } +// FetchStandTime requests the desk's standing time statistics from the controller. +// The command is sent twice as required by the protocol for reliability. +// Returns an error if the command transmission fails. func (j *Jiecang) FetchStandTime() error { - // Implements fetch_stand_time command if err := j.sendCommand(commands["fetch_stand_time"]); err != nil { return err } return j.sendCommand(commands["fetch_stand_time"]) } +// FetchAllTime requests the desk's total usage time statistics from the controller. +// The command is sent twice as required by the protocol for reliability. +// Returns an error if the command transmission fails. func (j *Jiecang) FetchAllTime() error { - // Implements fetch_all_time command if err := j.sendCommand(commands["fetch_all_time"]); err != nil { return err } diff --git a/pkg/jiecang/memory.go b/pkg/jiecang/memory.go index 4b3e23e..ee2dd97 100644 --- a/pkg/jiecang/memory.go +++ b/pkg/jiecang/memory.go @@ -9,7 +9,25 @@ import ( ) // GoToMemory moves the desk to the specified memory preset (1-3). -// The operation can be cancelled via the provided context. +// The operation polls the current height until the target preset height is reached +// or the context is cancelled. +// +// Parameters: +// - ctx: Context for cancellation and timeout control +// - memoryNum: Memory preset number (1, 2, or 3) +// +// Returns an error if: +// - memoryNum is not in the valid range (1-3) +// - command transmission fails +// - context is cancelled (operation stops gracefully, returns nil) +// +// Example: +// +// ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) +// defer cancel() +// if err := desk.GoToMemory(ctx, 1); err != nil { +// log.Fatal(err) +// } func (j *Jiecang) GoToMemory(ctx context.Context, memoryNum int) error { if memoryNum < 1 || memoryNum > 3 { return fmt.Errorf("invalid memory number %d (must be 1-3)", memoryNum) @@ -50,25 +68,55 @@ func (j *Jiecang) GoToMemory(ctx context.Context, memoryNum int) error { return nil } -// GoToMemory1 moves the desk to memory preset 1. -// Deprecated: Use GoToMemory(ctx, 1) instead. +// GoToMemory1 moves the desk to the height saved in memory preset 1. +// The operation polls the current height until the target is reached or +// the context is cancelled. +// +// Deprecated: Use GoToMemory(ctx, 1) instead for a unified interface. +// +// Returns an error if command transmission fails or the context is cancelled. func (j *Jiecang) GoToMemory1(ctx context.Context) error { return j.GoToMemory(ctx, 1) } -// GoToMemory2 moves the desk to memory preset 2. -// Deprecated: Use GoToMemory(ctx, 2) instead. +// GoToMemory2 moves the desk to the height saved in memory preset 2. +// The operation polls the current height until the target is reached or +// the context is cancelled. +// +// Deprecated: Use GoToMemory(ctx, 2) instead for a unified interface. +// +// Returns an error if command transmission fails or the context is cancelled. func (j *Jiecang) GoToMemory2(ctx context.Context) error { return j.GoToMemory(ctx, 2) } -// GoToMemory3 moves the desk to memory preset 3. -// Deprecated: Use GoToMemory(ctx, 3) instead. +// GoToMemory3 moves the desk to the height saved in memory preset 3. +// The operation polls the current height until the target is reached or +// the context is cancelled. +// +// Deprecated: Use GoToMemory(ctx, 3) instead for a unified interface. +// +// Returns an error if command transmission fails or the context is cancelled. func (j *Jiecang) GoToMemory3(ctx context.Context) error { return j.GoToMemory(ctx, 3) } -// SaveMemory saves the current height to the specified memory preset (1-3). +// SaveMemory saves the current desk height to the specified memory preset (1-3). +// The current height is stored in the controller's non-volatile memory +// and can be recalled later using GoToMemory. +// +// Parameters: +// - memoryNum: Memory preset number (1, 2, or 3) +// +// Returns an error if: +// - memoryNum is not in the valid range (1-3) +// - command transmission fails +// +// Example: +// +// if err := desk.SaveMemory(1); err != nil { +// log.Fatal(err) +// } func (j *Jiecang) SaveMemory(memoryNum int) error { if memoryNum < 1 || memoryNum > 3 { return fmt.Errorf("invalid memory number %d (must be 1-3)", memoryNum) @@ -84,26 +132,48 @@ func (j *Jiecang) SaveMemory(memoryNum int) error { return nil } -// SaveMemory1 saves the current height to memory preset 1. -// Deprecated: Use SaveMemory(1) instead. +// SaveMemory1 saves the current desk height to memory preset 1. +// The current height is stored in the controller's non-volatile memory +// and can be recalled later using GoToMemory1 or GoToMemory(ctx, 1). +// +// Deprecated: Use SaveMemory(1) instead for a unified interface. +// +// Returns an error if the command transmission fails. func (j *Jiecang) SaveMemory1() error { return j.SaveMemory(1) } -// SaveMemory2 saves the current height to memory preset 2. -// Deprecated: Use SaveMemory(2) instead. +// SaveMemory2 saves the current desk height to memory preset 2. +// The current height is stored in the controller's non-volatile memory +// and can be recalled later using GoToMemory2 or GoToMemory(ctx, 2). +// +// Deprecated: Use SaveMemory(2) instead for a unified interface. +// +// Returns an error if the command transmission fails. func (j *Jiecang) SaveMemory2() error { return j.SaveMemory(2) } -// SaveMemory3 saves the current height to memory preset 3. -// Deprecated: Use SaveMemory(3) instead. +// SaveMemory3 saves the current desk height to memory preset 3. +// The current height is stored in the controller's non-volatile memory +// and can be recalled later using GoToMemory3 or GoToMemory(ctx, 3). +// +// Deprecated: Use SaveMemory(3) instead for a unified interface. +// +// Returns an error if the command transmission fails. func (j *Jiecang) SaveMemory3() error { return j.SaveMemory(3) } -// Reads the response of the controller containing height settings of -// memory presets. +// readMemoryPreset decodes a memory preset height value from the controller response. +// The function extracts the height from bytes 4-5 of the response buffer and converts +// it from millimeters (protocol format) to centimeters (application format). +// +// Parameters: +// - buf: Response buffer from the controller. Expected format: +// [0xf2, 0xf2, type, 0x02, highByte, lowByte, checksum, 0x7e] +// +// Returns the preset height in centimeters, or 0 if the response type is invalid. func readMemoryPreset(buf []byte) uint8 { if buf[3] == 0x02 { preset := int(buf[4])*256 + int(buf[5])