lcm

package module
v0.0.0-...-ee3b374 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 29, 2022 License: MIT Imports: 11 Imported by: 0

README

lcm

The lcm project implements the ASUSTOR NAS LCD serial port communication protocol. ASUSTOR has a daemon running on their NASes (lcmd) which is responsible for controlling the LCD and buttons, this project aims to reimplement some of this functionality.

It can be used to write text on the LCD, turn the display on and off and read button presses.

Status

Most of the protocol has been documented in code and the library implementation is quite robust and tries to handle edge cases and error states.

For a standalone program see cmd/openlcmd. The openlcmd program implements some basic functionality and can be used as a reference implementation for more advanced programs. The underlying library is implemented in the lcm package.

All the naming in this project is guesswork, some messages may be named incorrectly and have a hidden or unknown purpose.

Installation

git clone https://github.com/mafredri/lcm
cd lcm
go build ./cmd/openlcmd
sudo cp openlcmd /usr/local/sbin/openlcmd
sudo cp cmd/openlcmd/systemd/openlcmd.service /etc/systemd/system/openlcmd.service
sudo systemctl enable openlcmd
sudo systemctl start openlcmd

NOTE: openlcmd does not have to run as root, but the user will need to have read/write access to /dev/ttyS1.

Why?

I stopped using ADM and switched to plain Debian on my AS-604T and AS-6204T and the ASUSTOR control software is not portable. So I wrote my own.

This project was already working in 2018 but wasn't published until 2021 after a big refactor.

Project structure

  • lcm
    • The LCM library, implements the protocol
  • lcm/cmd/openlcmd
    • Daemon that runs on the ASUSTOR NAS and handles updating of the LCD and reacting to button presses
    • Exposes buttons as virtual keyboard (uinput)
    • Can power cycle the LCD via GPIO

Research

Initially based on captured logs (see mafredri/asustor_as-6xxt/lcmd-logs), later reverse-engineering of ASUSTOR lcmd to determine.

Documentation

Overview

Package lcm implements the serial communication protocol for the ASUSTOR LCD display. This includes controlling and updating and listening for button presses.

LCM data format:

MESSAGE_TYPE DATA_LENGTH FUNCTION [[DATA]...] [CRC]

Index

Constants

View Source
const (
	// DefaultReplyTimeout defines how long we wait for a reply,
	// usually one is received in under 10ms. We keep this timeout
	// fairly tight because a longer delay rarely helps.
	//
	// The ASUSTOR daemon resends messages after 100ms if no
	// response is received. But even this can leads to deadlocks
	// where the same error will be echoed back time and time again.
	DefaultReplyTimeout = 15 * time.Millisecond
	// DefaultRetryLimit defines how many times a command will be
	// retried until giving up. Given the default reply timeout,
	// this could lead to nothing happening on the screen for about
	// 750ms.
	//
	// ASUSTOR tries up to 100 times, however, this rarely helps
	// clear up the communication error.
	DefaultRetryLimit = 50
	// DefaultWriteDelay defines how long to wait before writing the
	// next message. This is used both when writing commands and
	// responding to commands from the display.
	//
	// The ASUSTOR lcmd binary uses 15ms and 45ms sleeps between
	// certain commands, but this seems excessive.
	DefaultWriteDelay = 250 * time.Microsecond
)
View Source
const DefaultTTY = "/dev/ttyS1"

DefaultTTY represents the default serial tty for LCM.

Variables

This section is empty.

Functions

func Scroll

func Scroll(line DisplayLine, text string) (next func() (raw Message, start, done bool))

Scroll the text on the display. Each invocation of next() will return a message to send. The start value indicates that the text is in the starting position and the done value indicates one rotation has completed. Done becomes true one step before start meaning that the starting position is not yet reached (we have scrolled to the end).

next := lcm.Scroll(lcm.DisplayTop, "This text will scroll")
for {
	b, start, done := next()
	send(m, b)
	if start {
		time.Sleep(2 * time.Second)
	} else {
		time.Sleep(1 * time.Second)
	}
	if start && done {
		break
	}
}

func ShowAllCharCodes

func ShowAllCharCodes() (next func() (line1, line2 Message, start, done bool), goBack func())

ShowAllCharCodes allows all character codes to be

Types

type Button

type Button byte

Button represents a LCM button.

const (
	Up Button = iota + 1
	Down
	Back
	Enter
)

Button enums.

func (Button) String

func (i Button) String() string

type DisplayLine

type DisplayLine int

DisplayLine specifies which line to write the text on.

const (
	DisplayTop DisplayLine = iota
	DisplayBottom
)

DisplayLine enums.

type Function

type Function byte

Function represents the message function.

const (
	Fon      Function = 0x11
	Fclear   Function = 0x12
	Fversion Function = 0x13

	Fstatus Function = 0x22
	Fchar   Function = 0x25
	Fclear2 Function = 0x26
	Ftext   Function = 0x27
	Fbutton Function = 0x80
)

type LCM

type LCM struct {
	// contains filtered or unexported fields
}

LCM represents the ASUSTOR Liquid Crystal Monitor.

func Open

func Open(tty string, opt ...OpenOption) (*LCM, error)

Open opens the serial port for LCM.

func (*LCM) Close

func (m *LCM) Close() error

Close the serial connection.

func (*LCM) Recv

func (m *LCM) Recv() Message

Recv messages sent from the display.

func (*LCM) Send

func (m *LCM) Send(msg Message) error

Send messages to the display. Note that checksum should be omitted, it is handled transparently as part of the protocol implementation.

TODO(mafredri): Add support for functional arguments:

m.Send(msg, lcm.WithRetryLimit(100), lcm.WithReplyTimeout(5 * time.Millisecond))

type Logger

type Logger interface {
	Printf(format string, v ...interface{})
}

Logger represents a generic logger (e.g. from the log package).

type Message

type Message []byte

Message represents a serial port message with common bits easily accessible.

var (

	// DisplayOn turns the display on.
	DisplayOn Message = []byte{byte(Command), 0x01, byte(Fon), 0x01}
	// DisplayOff turns the display off.
	DisplayOff Message = []byte{byte(Command), 0x01, byte(Fon), 0x00}
	// ClearDisplay clears the current text from the display.
	// Called during re-initialization in lcmd.
	ClearDisplay Message = []byte{byte(Command), 0x01, byte(Fclear), 0x01}
	// ClearDisplayPrefix clears the screen and its behavior is
	// altered by AlterClearDisplayPrefix.
	//
	// It is unused in lcmd.
	ClearDisplayPrefix Message = []byte{byte(Command), 0x01, byte(Fclear2), 0x00}
	// DisplayStatus has an unknown purpose. It is issued after
	// DisplayOn in the init-routine and sometimes before/after
	// updating the text.
	//
	// It could have some other purpose, like SetClearDisplayPrefix.
	DisplayStatus Message = []byte{byte(Command), 0x01, byte(Fstatus), 0x00}
	// RequestVersion reports the MCU version via command.
	// The only observed version number so far is 0.1.2 on both
	// AS604T and AS6204T.
	//
	// Note: Issuing this request takes 200+ms and acknowledging
	// that we received the message often results in the display
	// thinking we re-requested the version. ASUSTOR does not seem
	// to use this, perhaps there is only one version out there.
	//
	// => 0xf001130105
	// <= 0xf101130005 (ack)
	// <= 0xf0031300010209 (version)
	RequestVersion Message = []byte{byte(Command), 0x01, byte(Fversion), 0x01}
)

Known commands (for sending to display).

var (
	// UnknownReply0x10, unused in the lcmd binary. We don't know
	// the purpose of the 0x10 function, but it may be possible for
	// the display to issue this command, in which case this would
	// be the (error) response.
	UnknownReply0x10 Message = []byte{byte(Reply), 0x01, 0x10, 0x02}
	// UnknownReply0x10, unused in the lcmd binary. This is an error
	// reply issued by the display as a response to the On function,
	// however, it's purpose in the lcmd binary is unknown.
	UnknownReply0x11 Message = []byte{byte(Reply), 0x01, byte(Fon), 0x02}
)

Replies are acknowledgements to commands, when the payload bit is zero, the command was successfully received (and applied), when it's non-zero, there was an error.

We don't know exactly what the different non-zero bits mean other than an error occurred. The bits are usually either 0x02 or 0x04, but even ASUSTORs lcmd binary does not care, it simply re-issues commands on any non-zero bit.

Documented here are some mysteries found in the lcmd binary.

var UnknownCommand0x23 Message = []byte{byte(Command), 0x02, 0x23, 0x00, 0x00}

UnknownCommand0x23, unused. Values come from function arguments.

Observed behavior: Nothing.

func SetClearDisplayPrefix

func SetClearDisplayPrefix(method int) Message

SetClearDisplayPrefix changes the behavior of ClearDisplayPrefix.

Known values and behavior of ClearDisplayPrefix: * 0 = Clear screen (fully) * 1 = Clear screen with underscore * 2 = Clear screen, blink between underscore and block

In lcmd this command is sometimes used after the text for line 0 has been set and before line 1 is cleared with spaces. Unless it has other unobserved behaviors, it's probably unused in practice.

func SetDisplay

func SetDisplay(line DisplayLine, indent int, text string) (raw Message, err error)

SetDisplay allows 16 characters to be written on either the top or bottom line, and indent can be used in which case not all characters in the message will be visible.

When using indent, it's a good idea to fill the display with spaces before (first) use so that there is no stray characters in the beginning. This can be achieved by first setting the display to the empty string as it will be padded with spaces.

SetDisplay(DisplayTop, 0, "")
SetDisplay(DisplayTop, 2, "My message")

func SetDisplayCharacter

func SetDisplayCharacter(line DisplayLine, column int, char byte) (Message, error)

SetDisplayCharacter writes a single character onto the display in the specificed location.

In lcmd, it is used by Lcmd_User_Menu_Ctl.

func (Message) Check

func (m Message) Check() error

Check that the message is valid (message must not include a checksum).

func (Message) Function

func (m Message) Function() Function

Function returns the message function.

func (Message) Ok

func (m Message) Ok() bool

func (Message) ReplyOk

func (m Message) ReplyOk() Message

ReplyOk returns a valid Reply for a Command.

func (Message) Type

func (m Message) Type() Type

Type returns the message type.

func (Message) Value

func (m Message) Value() []byte

type OpenOption

type OpenOption func(*openOptions)

OpenOption configures LCM during open.

func EnableProtocolAckReply

func EnableProtocolAckReply() OpenOption

EnableProtocolAckReply specifies if LCM should send acknowledgement replies to the screen when it sends us a command (e.g. button press or firmware version).

If the screen sent us a command, technically we should acknowledge it by sending a reply indicating it was successful. However, it often causes later commands (from us) to become corrupt. The frequency of the corruption can be lowered with delays, but then again, it seems like the display does not care if we reply or not.

func WithLogger

func WithLogger(l Logger) OpenOption

WithLogger sets the logger used by LCM (default none).

type Power

type Power struct {
	// contains filtered or unexported fields
}

Power management via GPIO line.

func NewPower

func NewPower(consumer string) (*Power, error)

NewPower initializes the GPIO line for powering LCM on and off.

func (*Power) Close

func (p *Power) Close() error

Close the GPIO line.

func (*Power) Cycle

func (p *Power) Cycle() (initialAnimationComplete <-chan time.Time)

Cycle the LCM power and return a channel that blocks until initial animation is completed.

func (*Power) Off

func (p *Power) Off()

Off turns the LCM off.

func (*Power) On

func (p *Power) On()

On turns the LCM on.

type Type

type Type byte

Type represents the message type.

const (
	Command Type = 0xF0
	Reply   Type = 0xF1
)

LCM message types.

Directories

Path Synopsis
cmd
lcm-monitor
lcm-monitor intercepts the communication between the ASUSTOR LCD daemon (lcmd) and the LCD display and saves the input (from LCD) and output (from lcmd) to files.
lcm-monitor intercepts the communication between the ASUSTOR LCD daemon (lcmd) and the LCD display and saves the input (from LCD) and output (from lcmd) to files.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL