package org.lcdproc.lcdjava; import java.awt.event.ActionEvent; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.Socket; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class to control access to an LCDproc LCDd daemon. *
Copyright (c) 2004-2005 Darren Greaves. * @author Darren Greaves * @version $Id: LCD.java,v 1.3 2008-07-06 15:38:34 boncey Exp $ */ public class LCD implements LCDListener { /** * Version details. */ public static final String CVSID = "$Id: LCD.java,v 1.3 2008-07-06 15:38:34 boncey Exp $"; private static final Logger _log = LoggerFactory.getLogger(LCD.class); /** * How often to poll for changes. */ private static final int POLL = 100; /** * How many times to poll the server before giving up. */ private static final int POLL_REPEAT = 30; /** * The pattern for matching a grouping of one or more numbers and . * characters. */ private static final String PATTERN_NUMERIC = "([\\d\\.]+)"; /** * The pattern for matching the server version. *
All numeric for stable, text for CVS version.
*/
private static final String PATTERN_VERSION = "([\\w\\-\\d\\.]+)";
/**
* The pattern for matching and grouping the init string returned by LCDd.
*/
private static final String PATTERN_INIT =
"connect LCDproc " + PATTERN_VERSION +
" protocol " + PATTERN_NUMERIC +
" lcd wid " + PATTERN_NUMERIC +
" hgt " + PATTERN_NUMERIC +
" cellwid " + PATTERN_NUMERIC +
" cellhgt " + PATTERN_NUMERIC;
/**
* The response from LCDd that indicates it didn't understand our command.
*/
public static final String RESPONSE_ERROR = "huh?";
/**
* The response from LCDd that indicates it is listening to a Screen.
*/
public static final String RESPONSE_IGNORE = "ignore";
/**
* The response from LCDd that indicates it is ignoring a Screen.
*/
public static final String RESPONSE_LISTEN = "listen";
/**
* The command to send to the server to initiate communication.
*/
private static final String CMD_INIT = "hello";
/**
* The command to send to the server to identify ourselves.
*/
private static final String CMD_CLIENT_SET = "client_set -name ";
/**
* The command to send to the server to add a Screen.
*/
private static final String CMD_SCREEN_ADD = "screen_add ";
/**
* The command to send to the server to set (modify) a Screen.
*/
private static final String CMD_SCREEN_SET = "screen_set ";
/**
* The command to send to the server to remove a Screen.
*/
private static final String CMD_SCREEN_DEL = "screen_del ";
/**
* The command to send to the server to add a Widget.
*/
public static final String CMD_WIDGET_ADD = "widget_add ";
/**
* The command to send to the server to set (modify) a Widget.
*/
public static final String CMD_WIDGET_SET = "widget_set ";
/**
* The command to send to the server to remove a Widget.
*/
public static final String CMD_WIDGET_DEL = "widget_del ";
/**
* The command to send to the server to add a menu item.
*/
public static final String CMD_MENU_ADD = "menu_add_item ";
/**
* The command to send to the server to remove a menu item.
*/
public static final String CMD_MENU_DEL = "menu_del_item ";
/**
* The command to send to the server to set (modify) a MenuItem.
*/
public static final String CMD_MENU_SET = "menu_set_item ";
/**
* The command to send to the server to set a menu as main menu
*/
public static final String CMD_MENU_SET_MAIN = "menu_set_main ";
/**
* The response from LCDd that indicates an action menu item was selected.
*/
private static final String EVENT_SELECT = "select";
/**
* The response from LCDd that indicates an menu item was updated.
*/
private static final String EVENT_UPDATE = "update";
/**
* The response from LCDd that indicates a slider was moved to the right.
*/
private static final String EVENT_PLUS = "plus";
/**
* The response from LCDd that indicates a slider was moved to the left.
*/
private static final String EVENT_MINUS = "minus";
/**
* The protocol version that we know how to deal with.
*/
private static final String PROTOCOL_VERSION = "0.3";
/**
* This is the maximum data that can be sent at once.
* This is calculated from the following code in lcdproc-0.4.5/server/sock.c
* #define MAXMSG 8192
* ...
* else if (nbytes > (MAXMSG - (MAXMSG / 8))
* {
* sock_send_string
* (filedes, "huh? Too much data received... quiet down!\n");
* }
*/
private static final int MAX_DATA_LENGTH = 7168;
/**
* The measurement of a LCD frame, documented as one eight of a
* second.
*/
public static final float FRAME = 0.125f;
/**
* The Socket used to talk and listen to the LCDd server.
*/
private Socket _socket;
/**
* The Writer we send all data to the server with.
*/
private BufferedWriter _out;
/**
* Thread that listens for responses from the server in a non-blocking
* manner.
*/
private LCDSocketPoller _poller;
/**
* The software version of LCDd.
*/
private String _version;
/**
* The protocol version.
*/
private String _protocolVersion;
/**
* The width in characters of the LCD device.
*/
private int _width;
/**
* The height in characters of the LCD device.
*/
private int _height;
/**
* The cell width of the LCD device.
*/
private int _cellWidth;
/**
* The cell height of the LCD device.
*/
private int _cellHeight;
/**
* The Map of Screens, indexed by the Screen id.
*/
private final Map This is used internally to ensure Screens have unique ids.
*/
private int _screenCounter;
/**
* The root of the client's menu
*/
private final Submenu _rootMenu;
/**
* Public constructor.
* @param host the LCDd host.
* @param port the LCDd port.
* @throws LCDException if there was a problem connecting to the server.
*/
public LCD(String host, int port)
throws LCDException
{
this(host, port, "lcdjava/1.0");
}
/**
* Public constructor.
* @param host the LCDd host.
* @param port the LCDd port.
* @param clientName the name of this client as shown to the user
* @throws LCDException if there was a problem connecting to the server.
*/
public LCD(String host, int port, String clientName)
throws LCDException
{
_screens = new HashMap<>();
_rootMenu = new Submenu(this);
try
{
String response = connect(host, port, clientName);
if (response == null)
{
throw new LCDException(
"Unable to get a response from the server at " +
host + ":" + port);
}
else if (response.startsWith(RESPONSE_ERROR))
{
throw new LCDException(
"Received error response from the server at " +
host + ":" + port);
}
parseInitString(response);
}
catch (IOException e)
{
shutdown();
throw new LCDException(e);
}
catch (LCDException e)
{
shutdown();
throw e;
}
_log.info("Connected");
}
/**
* Get the version.
* @return the version.
*/
public String getVersion()
{
return _version;
}
/**
* Get the protocolVersion.
* @return the protocolVersion.
*/
public String getProtocolVersion()
{
return _protocolVersion;
}
/**
* Get the width.
* @return the width.
*/
public int getWidth()
{
return _width;
}
/**
* Get the height.
* @return the height.
*/
public int getHeight()
{
return _height;
}
/**
* Get the cellWidth.
* @return the cellWidth.
*/
public int getCellWidth()
{
return _cellWidth;
}
/**
* Get the cellHeight.
* @return the cellHeight.
*/
public int getCellHeight()
{
return _cellHeight;
}
/**
* The listen/ignore state of a Screen has been notified to us by LCDd.
* @param screenId the id of the Screen.
* @param listening There is no need to call this, call {@link Screen#activate()} instead.
* @param screen the Screen to add.
*/
protected synchronized void addScreen(Screen screen)
{
Integer id = screen.getId();
if (!_screens.containsKey(id))
{
_screens.put(id, screen);
write(LCD.CMD_SCREEN_ADD + id);
updateScreen(screen);
}
}
/**
* Update this screen to the LCD server.
* @param screen the Screen to update.
*/
protected synchronized void updateScreen(Screen screen)
{
Integer id = screen.getId();
if (_screens.containsKey(id))
{
write(LCD.CMD_SCREEN_SET + screen.getData());
}
}
/**
* Remove a Screen from the server.
* @param screen the Screen to remove.
* @return the removed Screen, or null if not removed.
*/
protected synchronized Screen removeScreen(Screen screen)
{
return removeScreen(screen.getId());
}
/**
* Remove a Screen from the server.
* @param screenId the Screen id to remove.
* @return the removed Screen, or null if not removed.
*/
protected synchronized Screen removeScreen(int screenId)
{
write(CMD_SCREEN_DEL + screenId);
return _screens.remove(screenId);
}
public Submenu getRootMenu() {
return _rootMenu;
}
/**
* Parse the init string and split up into groups.
* @param init the init string.
* @throws LCDException if the protocol version is one we don't know about.
*/
private void parseInitString(String init)
throws LCDException
{
Pattern p = Pattern.compile(PATTERN_INIT);
Matcher m = p.matcher(init);
if (m.matches())
{
_version = m.group(1);
_protocolVersion = m.group(2);
_width = Integer.parseInt(m.group(3));
_height = Integer.parseInt(m.group(4));
_cellWidth = Integer.parseInt(m.group(5));
_cellHeight = Integer.parseInt(m.group(6));
if (!PROTOCOL_VERSION.equals(_protocolVersion))
{
throw new LCDException("Incorrect protocol version, " +
"we only understand version " + PROTOCOL_VERSION);
}
}
else
{
throw new LCDException(
"Unable to parse parameters from LCDd response '" + init + "'");
}
}
/**
* Connect to the LCDd server.
* @param host the hostname to connect to.
* @param port the port to connect to.
* @param clientName the name of this client as shown to the user
* @return the response from the server.
* @throws IOException in case of a network problem.
*/
private String connect(String host, int port, String clientName)
throws IOException
{
_socket = new Socket(host, port);
BufferedReader in = new BufferedReader(new InputStreamReader(
_socket.getInputStream()));
_out = new BufferedWriter(new OutputStreamWriter(
_socket.getOutputStream()));
_poller = new LCDSocketPoller(in, this);
_poller.start();
write(CMD_INIT);
String response = null;
for (int i = 0; i < POLL_REPEAT && response == null; i++)
{
synchronized (this)
{
try
{
Thread.sleep(POLL);
response = _poller.getLastLine();
}
catch (InterruptedException e)
{
// Do nothing
}
}
}
write(CMD_CLIENT_SET + clientName);
return response;
}
/**
* Write the data to the server.
* @param text the text to write.
*/
protected void write(String text)
{
try
{
String tmpString = text;
if (text.length() >= MAX_DATA_LENGTH)
{
_log.error("Text too long: " + text.length() + ", truncating");
tmpString = text.substring(0, MAX_DATA_LENGTH - 1);
}
_out.write(tmpString);
_out.newLine();
_out.flush();
}
catch (IOException e)
{
throw new LCDException(e);
}
}
/**
* Return a String representing this object.
* @return a String representing this object.
*/
public String toString()
{
return "Version = " + _version +
"; protocol version = " + _protocolVersion +
"; width = " + _width +
"; height = " + _height +
"; cell width = " + _cellWidth +
"; cell height = " + _cellHeight;
}
}
true if the server is listening to us,
* false if not.
*/
public void setListenStatus(int screenId, boolean listening)
{
Screen screen = _screens.get(screenId);
if (screen != null)
{
screen.setListening(listening);
}
else
{
_log.warn("Receiving signal " +
(listening ? "listen" : "ignore") +
" about screen id " +
screenId + " that we don't know anything about");
}
}
/**
* The user interacted with a menu
* @param menuId the id of the menu item.
* @param eventType the type of the event that occurred
* @param value the value returned or nullwhen not available
*/
@Override
public void menuAction(String menuId, String eventType, String value) {
String menuParts[] = menuId.split("_");
StringBuilder id = new StringBuilder();
MenuItem menu = _rootMenu;
for (String part : menuParts) {
if (!"".equals(part)) {
id.append('_');
id.append(part);
if (menu instanceof Submenu) {
menu = ((Submenu)menu).getMenuItem(id.toString());
} else {
break;
}
if (menu == null) {
break;
}
}
}
if (menu != null) {
switch (eventType) {
case LCD.EVENT_SELECT:
if (menu instanceof ActionMenuItem) {
((ActionMenuItem) menu).notifyActionPerformed(new ActionEvent(menu, ActionEvent.ACTION_PERFORMED, menu.getID()));
}
break;
case LCD.EVENT_UPDATE:
if (menu instanceof CheckboxMenuItem) {
if ("on".equals(value)) {
((CheckboxMenuItem) menu).setValueNoUpdate(CheckboxMenuItem.CheckboxValue.On);
} else if ("off".equals(value)) {
((CheckboxMenuItem) menu).setValueNoUpdate(CheckboxMenuItem.CheckboxValue.Off);
} else if ("gray".equals(value)) {
((CheckboxMenuItem) menu).setValueNoUpdate(CheckboxMenuItem.CheckboxValue.Gray);
}
((CheckboxMenuItem) menu).notifyActionPerformed(new ActionEvent(menu, ActionEvent.ACTION_PERFORMED, menu.getID()));
} else if (menu instanceof RingMenuItem) {
((RingMenuItem) menu).setValueNoUpdate(Integer.parseInt(value));
((RingMenuItem) menu).notifyActionPerformed(new ActionEvent(menu, ActionEvent.ACTION_PERFORMED, menu.getID()));
} else if (menu instanceof NumericMenuItem) {
((NumericMenuItem) menu).setValueNoUpdate(Integer.parseInt(value));
((NumericMenuItem) menu).notifyActionPerformed(new ActionEvent(menu, ActionEvent.ACTION_PERFORMED, menu.getID()));
} else if (menu instanceof AlphaMenuItem) {
((AlphaMenuItem) menu).setValueNoUpdate(value);
((AlphaMenuItem) menu).notifyActionPerformed(new ActionEvent(menu, ActionEvent.ACTION_PERFORMED, menu.getID()));
} else if (menu instanceof IpMenuItem) {
((IpMenuItem) menu).setValueNoUpdate(value);
((IpMenuItem) menu).notifyActionPerformed(new ActionEvent(menu, ActionEvent.ACTION_PERFORMED, menu.getID()));
}
break;
case LCD.EVENT_PLUS:
case LCD.EVENT_MINUS:
if (menu instanceof SliderMenuItem) {
int v = Integer.parseInt(value);
if (v != ((SliderMenuItem) menu).getValue()) {
((SliderMenuItem) menu).setValueNoUpdate(v);
((NumericMenuItem) menu).notifyActionPerformed(new ActionEvent(menu, ActionEvent.ACTION_PERFORMED, menu.getID()));
}
}
break;
}
}
}
/**
* Shut down the server, terminating any threads.
* @throws LCDException in case of a network problem.
*/
public void shutdown()
throws LCDException
{
_log.debug("Shutdown requested");
if (_poller != null)
{
_poller.interrupt();
_log.debug("Waiting for LCDSocketPoller to terminate...");
try {
_poller.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try
{
_log.debug("Closing socket");
if (_socket != null && !_socket.isClosed())
{
_socket.close();
}
}
catch (IOException e)
{
throw new LCDException(e);
}
}
/**
* Construct a new (unactivated) Screen.
* @param name the Screen name.
* @return the newly constructed Screen.
*/
public synchronized Screen constructScreen(String name)
{
return constructScreen(name, Screen.PRIORITY_HIDDEN);
}
/**
* Construct a new (unactivated) Screen.
* @param name the Screen name.
* @param priority the screen priority.
* @return the newly constructed Screen.
*/
public synchronized Screen constructScreen(String name, String priority)
{
return constructScreen(name, priority, false);
}
/**
* Construct and optionally activate a new Screen.
* @param name the Screen name.
* @param priority the screen priority.
* @param activate true to activate this Screen immediately,
* false to leave in unactivated.
* @return the newly constructed Screen.
*/
public synchronized Screen constructScreen(String name,
String priority,
boolean activate)
{
Screen screen = new Screen(this, _screenCounter, name);
_screenCounter++;
screen.setPriority(priority);
if (activate)
{
screen.activate();
}
return screen;
}
/**
* Add this screen to the LCD server.
*