package uk.ac.open.punchingbag; import gnu.io.CommPortIdentifier; import javax.swing.event.EventListenerList; import java.awt.Color; import java.awt.Rectangle; import java.awt.geom.Area; import java.io.IOException; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; /** * This class represents the punching bag, reads and sends data to and from the * arduino's (using the Arduino class), controlling the led's and reading the * state of the accelerometers and buttons. * * @author Christopher Baines * @author Adam Martindale */ public class PunchingBag implements Runnable { /** * Used by the run method to decide if to call the LEDListeners */ private boolean ledsChanged = false; /** * The number of rows of leds */ final public int ledHeight = 20; /** * The number of columns of leds */ final public int ledWidth = 9; /** * The byte array that is passed to the led drivers */ private byte[] rawLeds = new byte[6 * 8]; /** * The colour objects for each led, do not access directly as this does not * properly set ledsChanged */ private Color[][] leds = new Color[ledWidth][ledHeight]; /** * NOT USED YET, this should be sent to the arduino, but is not */ private byte[] ledGridIntensities = new byte[6]; /** * The number of rows of buttons */ final public int buttonHeight = 19; /** * The number of columns of buttons */ final public int buttonWidth = 8; /** * Contains the state of all the buttons as of the last loop in run */ private boolean[][] buttons = new boolean[buttonWidth][buttonHeight]; /** * Used by the run method to decide if to call the ButtonListeners */ boolean buttonsChanged = false; /** * Bottom accelerometer X value */ int bottomAccelX = 0; /** * Bottom accelerometer Y value */ int bottomAccelY = 0; /** * Top accelerometer X value */ int topAccelX = 0; /** * Top accelerometer Y value */ int topAccelY = 0; /** * Used to decide if to call the AccelListerners */ boolean acccelChanged = false; /** * The system time of the last run loop */ private long lastRefresh = 0; /** * Contains all running effects */ private ArrayList runningEffects = new ArrayList(); /** * Arduino managing the buttons and accelerometers */ Arduino buttonArduino = new Arduino(); /** * The device address of the button arduino (COM* on Windows, /dev/tty* on * linux) */ String buttonArduinoDeviceAddress = ""; // TODO: This should be cached // localy /** * The Arduino managing the led drivers */ Arduino ledArduino = new Arduino(); /** * The device address of the led arduino (COM* on Windows, /dev/tty* on * linux) */ String ledArduinoDeviceAddress = ""; // TODO: This should be cached localy /** * Turn on to activate the command line messages regarding the run loop * timing stuff */ boolean debugTimings = false; /** * Turn on to activate the command line messages regarding the serial stuff */ boolean debugSerial = false; /** * The only instance of this class, represents the PunchingBag */ static private PunchingBag bag = new PunchingBag(); /** * Use this to get a local pointer to the PunchingBag instance * * @return The PunchingBag instance */ public static PunchingBag getBag() { return bag; } public enum Direction { Right, Left, Up, Down }; /** * Is there a better class for this? One that it is not a swing class. */ private EventListenerList buttonListenerList = new EventListenerList(); /** * Is there a better class for this? One that it is not a swing class. */ private EventListenerList ledListenerList = new EventListenerList(); /** * Is there a better class for this? One that it is not a swing class. */ private EventListenerList accelListenerList = new EventListenerList(); /** * Private constructor, starts the run method. */ private PunchingBag() { Thread bagControlThread = new Thread(this); bagControlThread.setPriority(Thread.MAX_PRIORITY); bagControlThread.start(); } /** * Adds an ButtonListener to the bag. * * @param l * the ButtonListener to be added */ public void addButtonListener(ButtonListener l) { buttonListenerList.add(ButtonListener.class, l); } /** * Adds an LEDChangeListener to the bag. * * @param l * the LEDChangeListener to be added */ public void addLEDChangeListener(LEDListener l) { ledListenerList.add(LEDListener.class, l); } /** * Adds an AccelChangeListener to the bag. * * @param l * the AccelChangeListener to be added */ public void addAccelChangeListener(AccelListener l) { accelListenerList.add(AccelListener.class, l); } /** * Gets the Color of the led given by its x and y, it will be * one of: Color.red Color.yellow * Color.green Color.white * * @param x * The x coordinate of the led * @param y * The y coordinate of the led * @return */ public Color getLED(int x, int y) { return leds[x][y]; } /** * Used by methods in the PunchingBag class to change the led's array. To * simplify things it will accept any Color object, but if it's not one of * the supported (red,green,yellow), then it will just set it to white (the * none colour). * * @param x * The x coordinate of the led * @param y * The y coordinate of the led * @param colour * The desired colour of the led, as either Color.red, * Color.yellow, Color.green or Color,white. * @return true if something was changed, false if not */ private boolean setLEDInternal(int x, int y, Color colour) { if (x >= 0 && x < ledWidth && y >= 0 && y < ledHeight) { // Check if the // led being // set // exists if (leds[x][y] != colour) { // Check if the led is being changed if (!(colour == Color.red) && !(colour == Color.green) && !(colour == Color.yellow)) { if (leds[x][y] != Color.white) { leds[x][y] = Color.white; ledsChanged = true; } } else { if (leds[x][y] != colour) { leds[x][y] = colour; ledsChanged = true; } } } return true; } else { return false; // If not, return false } } /** * Sets a LED on the grid to the specified colour, either use this command * again to change it, or use the clearLEDS() function to clear * the entire grid. * * @param x * @param y * @param colour */ public void setLED(int x, int y, Color colour) { runningEffects.add(new Point(x, y, colour)); } /** * Starts an expanding circle at x and y. * * @param x * The x coordinate of the centre of the circle. * @param y * The y coordinate of the centre of the circle. * @param intensity * This controls the intensity of the circle, including colour * and (but not yet) weight. */ public void circleExpand(int x, int y, int intensity) { runningEffects.add(new CircleExpand(x, y, 100)); } /** * This controls the intensity of the circle, including colour and (but not * yet) weight. * * @param x * The x coordinate of the centre of the square * @param y * The y coordinate of the centre of the square * @param intensity * AFAIK, this does nothing yet * @param colour * The colour of the square */ public void squareExpand(int x, int y, int intensity, Color colour) { runningEffects.add(new SquareExpand(x, y, intensity, colour)); } /** * This should fill in a rectangle * * @param x * The x coordinate of the top right of the rectangle * @param y * The y coordinate of the top right of the * @param width * @param height * @param colour * @param time */ public void fillRect(int x, int y, int width, int height, Color colour, long time) { runningEffects.add(new FillRect(x, y, width, height, colour, time)); } /** * @param x * @param y * @param width * @param height * @param colour */ public void rect(int x, int y, int width, int height, Color colour) { runningEffects.add(new Rect(x, y, width, height, colour)); } /** * @param rect * @param time */ public void noise(Rectangle rect, long time) { runningEffects.add(new Noise(rect, System.currentTimeMillis() + time)); } /** * This represents an effect (a persistent visual artifact). The effects * system is meant to provide a simple way of drawing to the screen without * having to handle any of the refresh stuff. * * @author Christopher Baines * */ private abstract class Effect { /** * The system time of the last refresh */ long lastRefresh = 0; /** * Set when the effect should stop (use stop() to set). */ private boolean stop = false; /** * Used to stop the effect, removing it from the list of effects * (runningEffects) to be drawn. */ public void stop() { stop = true; } /** * The draw method for each effect, called every iteration in run. */ abstract public void draw(); } /** * Used by the setLED function, this draws a point on the screen. * * @author Christopher Baines * */ private class Point extends Effect { final int x; final int y; final Color colour; public Point(int x, int y, Color colour) { this.x = x; this.y = y; this.colour = colour; } public void draw() { setLEDInternal(x, y, colour); } } class FillRect extends Effect { final int x; final int y; final int width; final int height; final Color colour; final long time; public FillRect(int x, int y, int width, int height, Color colour, long time) { this.x = x; this.y = y; this.width = width; this.height = height; this.colour = colour; this.time = System.currentTimeMillis() + time; } public void draw() { fillRectInternal(x, y, width, height, colour); if (System.currentTimeMillis() >= time) { stop(); } } } class Rect extends Effect { final int x; final int y; final int width; final int height; final Color colour; public Rect(int x, int y, int width, int height, Color colour) { this.x = x; this.y = y; this.width = width; this.height = height; this.colour = colour; } public void draw() { drawRectInternal(x, y, width, height, colour); } } class Text extends Effect { final String string; final Area area; final Direction direction; final int speed; public Text(String string, Area area, Direction direction, int speed) { this.string = string; this.area = area; this.direction = direction; this.speed = speed; } public void draw() { // byte[] stringBytes = string.getBytes(); } } class CircleExpand extends Effect { final double x; final double y; int intensity; boolean drawnSomething = true; double currentRadius = 0.5; public CircleExpand(int x, int y, int intensity) { this.x = x + 0.5; this.y = y + 0.5; this.intensity = intensity; } public void draw() { Color colour; if (intensity >= 10) { colour = Color.red; } else if (intensity >= 5) { colour = Color.yellow; } else { colour = Color.green; } intensity -= 5; currentRadius += 0.4 + (currentRadius / 20); // currentRadius += 0.01; // longhand: currentRadius = currentRadius + 0.1 + (currentRadius / // 10); if (!drawCircle(x, y, currentRadius, colour)) stop(); } } class SquareExpand extends Effect { final int x; final int y; final Color colour; int intensity; boolean drawnSomething = true; int heightWidth = 2; public SquareExpand(int x, int y, int intensity, Color colour) { this.x = x; this.y = y; this.intensity = intensity; this.colour = colour; } public void draw() { heightWidth += 0.1 + 0.1 + (heightWidth / 20); if (!drawRectCenter(x, y, heightWidth, heightWidth, colour)) stop(); } } class Noise extends Effect { final Rectangle area; final long endTime; public Noise(Rectangle area, long endTime) { this.area = area; this.endTime = endTime; } @Override public void draw() { if (endTime >= System.currentTimeMillis()) { for (int y = area.y; y < (area.y + area.height); y++) { for (int x = area.x; x < (area.x + area.width); x++) { double random = Math.random(); if (random < 0.25) { setLEDInternal(x, y, Color.red); } else if (random < 0.5) { setLEDInternal(x, y, Color.yellow); } else if (random < 0.75) { setLEDInternal(x, y, Color.green); } else { setLEDInternal(x, y, Color.white); } } } } else { stop(); } } } public boolean drawRectCenter(int x, int y, int height, int width, Color colour) { if (height < 0) { height = 0; } if (width < 0) { width = 0; } if (height == 0 && width == 0) { return setLEDInternal(x, y, colour); } boolean doneSomething = false; int widthEx = 0; int heightEx = 0; do { if (setLEDInternal((x - (height / 2)) + widthEx, y - (height / 2), colour)) { doneSomething = true; } if (setLEDInternal(x - (height / 2), (y - (height / 2)) + heightEx, colour)) { doneSomething = true; } if (setLEDInternal((x - (height / 2)) + widthEx, y + (height / 2), colour)) { doneSomething = true; } if (setLEDInternal(x + (height / 2), (y - (height / 2)) + heightEx, colour)) { doneSomething = true; } if (heightEx < height) { heightEx++; } if (widthEx < width) { widthEx++; } } while (height >= 0 && width >= 0); return doneSomething; } public boolean drawRectInternal(int x, int y, int width, int height, Color colour) { if (height < 0) { height = 0; } if (width < 0) { width = 0; } if (height == 0 && width == 0) { return setLEDInternal(x, y, colour); } boolean doneSomething = false; int heightEx = 0; int widthEx = 0; boolean finished = true; do { if (setLEDInternal(x + widthEx, y, colour)) { doneSomething = true; } if (setLEDInternal(x, y + heightEx, colour)) { doneSomething = true; } if (setLEDInternal(x + widthEx, y + height, colour)) { doneSomething = true; } if (setLEDInternal(x + width, y + heightEx, colour)) { doneSomething = true; } if (heightEx < height) { finished = false; heightEx++; } if (widthEx < width) { finished = false; widthEx++; } } while (!finished); return doneSomething; } public boolean drawEllipse(int x, int y, int radx, int rady, Color colour) { if (radx == 0 && rady == 0) { return setLEDInternal(x, y, colour); } boolean doneSomething = false; int dx = 0, dy = rady; /* first quadrant from top left to bottom right */ int a2 = radx * radx, b2 = rady * rady; int err = b2 - (2 * rady - 1) * a2, e2; /* error value in the first step */ do { if (setLEDInternal(x + dx, y + dy, colour)) {/* I. Quadrant */ doneSomething = true; } if (setLEDInternal(x - dx, y + dy, colour)) {/* II. Quadrant */ doneSomething = true; } if (setLEDInternal(x - dx, y - dy, colour)) {/* III. Quadrant */ doneSomething = true; } if (setLEDInternal(x + dx, y - dy, colour)) {/* IV. Quadrant */ doneSomething = true; } e2 = 2 * err; if (e2 < (2 * dx + 1) * b2) { dx++; err += (2 * dx + 1) * b2; } if (e2 > -(2 * dy - 1) * a2) { dy--; err -= (2 * dy - 1) * a2; } } while (dy >= 0); while (dx++ < radx) { /* correction for flat ellipses (b=1) */ if (setLEDInternal(x + dx, y, colour)) { doneSomething = true; } if (setLEDInternal(x - dx, y, colour)) { doneSomething = true; } } return doneSomething; } public boolean drawCircle(double x, double y, double radius, Color colour) { boolean drawnSomething = false; int px; int py; for (double i = 0; i < 360; i++) { px = (int) Math.round(x + (radius * Math.cos(i))); py = (int) Math.round(y + (radius * Math.sin(i))); if (x >= 0 && x <= 8 && y >= 0 && y <= 19) { if (setLEDInternal(px, py, colour)) { drawnSomething = true; } } } return drawnSomething; } public boolean fillRectInternal(int x, int y, int height, int width, Color colour) { boolean doneSomething = false; for (int px = x; px < (x + height); px++) { for (int py = y; py < (y + width); py++) { if (setLEDInternal(px, py, colour)) { doneSomething = true; } } } return doneSomething; } private void calculateRawLeds() { // First clear everything Arrays.fill(rawLeds, (byte) 0); // First loop through the 5 easy arrays for (int grid = 0; grid < 5; grid++) { for (int x = 0; x < 8; x++) { for (int y = 0; y < 8; y++) { if ((y % 2) == 0) { if (leds[1 + x][(grid * 4) + (y / 2)] == Color.green || leds[1 + x][(grid * 4) + (y / 2)] == Color.yellow) { rawLeds[(grid * 8) + x] = (byte) (rawLeds[(grid * 8) + x] | (1 << (7 - y))); } } else { if (leds[1 + x][(grid * 4) + (y / 2)] == Color.red || leds[1 + x][(grid * 4) + (y / 2)] == Color.yellow) { rawLeds[(grid * 8) + x] = (byte) (rawLeds[(grid * 8) + x] | (1 << (7 - y))); } } } } } // TODO: Add support for the wierd column } /** * Clears the led grid */ private void clearLEDGrid() { for (int x = 0; x < ledWidth; x++) { for (int y = 0; y < ledHeight; y++) { leds[x][y] = Color.white; } } } /** * Clears the button grid */ private void clearButtonGrid() { for (int x = 0; x < buttonWidth; x++) { for (int y = 0; y < buttonHeight; y++) { buttons[x][y] = false; } } } /** * Stops all effects, this will remove them from the runningEffects list on * the next run iteration */ private void clearEffects() { for (Iterator iter = runningEffects.iterator(); iter.hasNext();) { ((Effect) iter.next()).stop(); } } /** * Clears the LED grid. */ public void clearLEDs() { clearLEDGrid(); clearEffects(); } /** * Each of the six grids (described at the top) can have an intensity set * between 0 and 15. * * @param grid * The number of the grid (0-5) * @param intensity * The desired intensity (0-15) */ public void setLEDGridIntensity(byte grid, byte intensity) { ledGridIntensities[grid] = intensity; } /** * Sets the intensity of all the grids. * * @param intensity * The desired intensity (0-15) */ public void setLEDIntensities(byte intensity) { if (intensity < 0) { Arrays.fill(ledGridIntensities, (byte) 0); } else if (intensity > 15) { Arrays.fill(ledGridIntensities, (byte) 15); } else { Arrays.fill(ledGridIntensities, intensity); } } /** * Generates a button pressed event * * @param x * The x coordinate of the button * @param y * The y coordinate of the button */ public void buttonPressed(int x, int y) { buttons[x][y] = true; buttonsChanged = true; } /** * Connect to both arduino's * * @return True if the connection to both was successful, false if not * @throws Exception */ public boolean connectToArduinos() throws Exception { return connectToButtonArduino() && connectToLEDArduino(); } /** * @return * @throws Exception */ public boolean connectToButtonArduino() throws Exception { return buttonArduino.connect(buttonArduinoDeviceAddress); } /** * @return * @throws Exception */ public boolean connectToLEDArduino() throws Exception { return ledArduino.connect(ledArduinoDeviceAddress); } /** * @return */ private boolean findArduinos() { // TODO: Detect OS and continue appropiately HashSet serialPorts = Arduino .getAvailableSerialPorts(); for (Iterator iter = serialPorts.iterator(); iter.hasNext();) { CommPortIdentifier comPort = (CommPortIdentifier) iter.next(); // HACK FOR WINDOWS TO IDENIFY PORTS LIKELY TO CONTAIN ARDUINO // (COM10 for instance). TODO: FIX try { System.out.println(comPort.getName()); // TODO: Connect and check what arduino this is } catch (Exception e) { e.printStackTrace(); } } return true; } /** * Processes the serial data regarding accelerations * * @param data The data string to be processed */ private void readAccelData(String data) { // System.out.println("Data: " + data); String[] nums = data.split(" "); for (int x = 1; x < nums.length; x++) { // Regex expression to strip newline at end (normally just 4th) if (nums[x].replaceAll("[^0-9]", "").length() == 0 || nums[x].replaceAll("[^0-9]", "").length() > 4) { System.err.println("Accel Data Error: " + data); continue; } int num; try { num = Integer.valueOf(nums[x].replaceAll("[^0-9]", "")); if (x == 1 && num != topAccelX) { topAccelX = num; acccelChanged = true; } else if (x == 2 && num != topAccelY) { topAccelY = num; acccelChanged = true; } else if (x == 3 && num != bottomAccelX) { bottomAccelX = num; acccelChanged = true; } else if (x == 4 && num != bottomAccelY) { bottomAccelY = num; acccelChanged = true; } } catch (Exception e) { e.printStackTrace(); System.err.println("Data: " + data); } } } /** * Processes the serial data regarding buttons * * @param data The data string to be processed */ private void readButtonData(String data) { // System.out.println("Data: " + data); if (data.replaceAll("[^0-9]", "").length() > 0) { // System.out.print(data); int num = Integer.valueOf(data.replaceAll("[^0-9]", "")); int x = ((num - 1) % 8); int y = (num - 1) / 8; System.out.println("X: " + x + " Y: " + y); if (x >= 0 && x < buttonWidth && y >= 0 && y < buttonHeight) { buttons[x][y] = true; buttonsChanged = true; } } } /** * Debugging utility function to print in binary byte b * * @param b */ private void printByte(byte b) { // System.out.println("Byte: " + b); String str; for (int j = 0; j < 8; j++) { byte val = (byte) (b & (1 << (7 - j))); // System.out.println("Val: " + val + " " + (1 << (7-j))); if (val > 0) { str = "1"; } else { str = "0"; } System.out.print(str); } } private void printlnByte(byte b) { printByte(b); System.out.println(""); } /* * The run method starts a loop which processes the serial data, effects and events * * @see java.lang.Runnable#run() */ public void run() { long timeToSleep = 10; // The time slept at the end of the loop (to maintain the refresh rate) while (true) { if (debugTimings) { System.out.println("Time since last refresh: " + (System.currentTimeMillis() - lastRefresh)); if ((System.currentTimeMillis() - lastRefresh) != 0) System.out .println("FPS: " + (1000 / (System.currentTimeMillis() - lastRefresh))); } if ((System.currentTimeMillis() - lastRefresh) > (1000 / 60)) { if (timeToSleep > 0) { timeToSleep--; } } else { timeToSleep++; } if (debugTimings) System.out.println("Sleeping: " + timeToSleep); lastRefresh = System.currentTimeMillis(); // System.out.println("R"); synchronized (leds) { clearLEDGrid(); // clearButtonGrid(); long beginTimeForEffects = System.currentTimeMillis(); synchronized (runningEffects) { // Should prevent // ConcurrentModificationException's // (havent managed to produce one // though for (Iterator iter = runningEffects.iterator(); iter .hasNext();) { Effect ef = (Effect) iter.next(); if (ef.stop) { iter.remove(); } else {// if ((ef.lastRefresh + (1000 / // ef.refreshRate)) <= // Systems.currentTimeMillis()) { ef.draw(); ef.lastRefresh = System.currentTimeMillis(); } } } // System.out.println("Effects: " // + (System.currentTimeMillis() - beginTimeForEffects)); } long beginTimeButtonIn = System.currentTimeMillis(); boolean doneA = false; boolean doneB = false; if (buttonArduino.connected()) { try { int read; // System.out.println("Reading selector"); while (!doneA || !doneB) { read = buttonArduino.read(); if (read == -1) break; if (debugSerial) System.out.println("Outside " + (char) read); if ((char) read == 'B') { if (debugSerial) System.out .println(" Got an B, begining reading the button data"); String str = ""; // StringBuilder sb = new StringBuilder(20); while (true) { read = buttonArduino.read(); if (read == '\n') break; str = str + String.valueOf((char) read); if (debugSerial) System.out.println("Reading button data: " + (char) read); // sb.append((char) read); } if (debugSerial) System.out .print("Finished reading button data because of newline " + (char) read); // System.out.println(""); doneB = true; readButtonData(str); } else if ((char) read == 'A') { if (debugSerial) System.out .println(" Got an A, begining reading the accel data"); String str = ""; // StringBuilder sb = new StringBuilder(20); while (true) { read = buttonArduino.read(); if (read == '\n') break; str = str + String.valueOf((char) read); if (debugSerial) System.out.println("Reading accel data " + (char) read); // sb.append((char) read); } // System.out.println(""); doneA = true; readAccelData(str); // String[] nums = sbString.trim().split(" "); // System.out.println(nums); // for (int x=0; x<4; x++) { // System.out.println(nums[x]); // } } } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (debugTimings) System.out.println("Button: " + (System.currentTimeMillis() - beginTimeButtonIn)); calculateRawLeds(); // String str; // for (int i = 0; i < (6 * 8); i++) { // printByte(rawLeds[i]); // } // System.out.println(""); // Arrays.fill(rawLeds, (byte) -42); long beginTimeLedOut = System.currentTimeMillis(); if (ledArduino.connected()) { // calculateRawLeds(); try { ledArduino.write(((byte) 108)); ledArduino.write(rawLeds); } catch (IOException e1) { e1.printStackTrace(); } } if (debugTimings) System.out.println("Leds: " + (System.currentTimeMillis() - beginTimeLedOut)); // Arrays.fill(rawLeds, (byte) -42); long beginTimeListeners = System.currentTimeMillis(); if (ledsChanged) { LEDListener[] LEDListeners = ledListenerList .getListeners(LEDListener.class); for (int i = 0; i < ledListenerList.getListenerCount(); i++) { LEDListeners[i].LEDchanged(); } } if (buttonsChanged) { ButtonListener[] buttonListeners = buttonListenerList .getListeners(ButtonListener.class); for (int i = 0; i < buttonListenerList.getListenerCount(); i++) { for (int x = 0; x < buttonWidth; x++) { for (int y = 0; y < buttonHeight; y++) { if (buttons[x][y]) { buttonListeners[i].buttonPressed(x, y); buttonListeners[i].contact(new Contact(System .currentTimeMillis(), x, y, bottomAccelX)); } } } } } if (debugTimings) System.out.println("Listeners: " + (System.currentTimeMillis() - beginTimeListeners)); clearButtonGrid(); try { Thread.sleep(timeToSleep); } catch (InterruptedException e) { e.printStackTrace(); } } } }