├── README.md ├── config └── gm1200_direkt ├── doc ├── FunkrufSlave_Dokumentation.odt ├── SDRPager_Interface.png ├── SDRPager_Interface.sch ├── SDRPager_with_GM1200.JPG ├── Wiring.SchDoc └── Wiring_to_Radio.pdf └── raspager-sdr ├── .gitignore ├── config ├── logging.properties ├── raspager.properties ├── raspager.sh └── raspager2.properties ├── pom.xml └── src ├── assembly └── bin.xml └── main ├── java └── de │ └── rwth_aachen │ └── afu │ └── raspager │ ├── ConfigKeys.java │ ├── Configuration.java │ ├── GpioView.java │ ├── Main.java │ ├── MasterServerFilter.java │ ├── Message.java │ ├── Pocsag.java │ ├── RasPagerService.java │ ├── RasPagerWindow.java │ ├── Scheduler.java │ ├── SearchScheduler.java │ ├── Server.java │ ├── ServerHandler.java │ ├── ThreadWrapper.java │ ├── TimeSlots.java │ ├── Transmitter.java │ └── sdr │ ├── AudioEncoder.java │ ├── GpioPortComm.java │ ├── SDRTransmitter.java │ └── SerialPortComm.java └── resources ├── MainWindow.properties ├── MainWindow_de_DE.properties ├── MainWindow_es_ES.properties └── pi_gpio.png /README.md: -------------------------------------------------------------------------------- 1 | # SDRPager 2 | POCSAG pager software based on soundcard generation of baseband signal 3 | 4 | ## Authors: 5 | * Ralf Wilke DH3WR, Aachen 6 | * Michael Delissen, Aachen 7 | * Marvin Menzerath, Aachen 8 | * Philipp Thiel DL6PT, Aachen 9 | 10 | This software is released free of charge under the Creative Commons License of type "by-nc-sa". No commercial use 11 | is allowed. 12 | The software licenses of the used libs as stated below apply in any case. 13 | 14 | 15 | ## Run 16 | * Install RXTX 17 | * `sudo apt-get install librxtx-java` 18 | * alternatively: follow [these](http://www.jcontrol.org/download/rxtx_de.html) instructions 19 | * Run `java -Djava.library.path=/usr/lib/jni -jar FunkrufSlave.jar` 20 | 21 | ## Example-Configuration 22 | ``` 23 | #[slave config] 24 | # Port 25 | port=1337 26 | 27 | # Allowed Masters, seperated by a space 28 | master=127.0.0.1 29 | 30 | # Correctionfactor 31 | correction=0.35 32 | 33 | # Serial: Port, Pin. If no Serial Port output is desired, change to serial = - DTR 34 | serial=/dev/ttyS0 DTR 35 | 36 | # GPIO-Pin: RasPi-Type / GPIO-Pin 37 | gpio=RaspberryPi_3B / GPIO 9 38 | 39 | # Additional configuration of Serial and GPIO 40 | 41 | # use gpio / serial 42 | use=gpio 43 | 44 | # 1 (yes) / 0 (no) 45 | invert=1 46 | 47 | # in ms 48 | delay=100 49 | 50 | # Sound Device (as it is identified by AudioSystem.getMixerInfo()) 51 | sounddevice=ALSA [default] 52 | 53 | # LogLevel 54 | # NORMAL = 0; DEBUG_CONNECTION = 1; DEBUG_SENDING = 2; DEBUG_TCP = 3; 55 | loglevel=0 56 | ``` 57 | 58 | ## Build 59 | * Java JDK 1.8 60 | * Libraries 61 | * [Pi4J](http://pi4j.com/) 62 | * `pi4j-core.jar` 63 | * `pi4j-device.jar` 64 | * `pi4j-gpio-extension.jar` 65 | * `pi4j-service.jar` 66 | * [RXTX](http://www.jcontrol.org/download/rxtx_de.html) 67 | -------------------------------------------------------------------------------- /config/gm1200_direkt: -------------------------------------------------------------------------------- 1 | #[slave config] 2 | # Port 3 | port=1337 4 | # Erlaubte Master getrennt durch Leerzeichen 5 | master=44.225.164.2 6 | # Korrekturfaktor 7 | correction=0.5 8 | # Serial: Port, Pin 9 | serial=- DTR 10 | # GPIO-Pin: RasPi-Typ / GPIO-Pin 11 | gpio=RaspberryPi_B_Rev1 / GPIO 0 12 | # Weitere Konfiguration von Serial und GPIO 13 | use=gpio 14 | invert=1 15 | delay=80 16 | # Sound Device 17 | sounddevice=ALSA [plughw:0,1] 18 | # LogLevel 19 | loglevel=0 20 | -------------------------------------------------------------------------------- /doc/FunkrufSlave_Dokumentation.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwth-afu/SDRPager/f9c0486dd5f89fb896445c0b016386e28f959667/doc/FunkrufSlave_Dokumentation.odt -------------------------------------------------------------------------------- /doc/SDRPager_Interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwth-afu/SDRPager/f9c0486dd5f89fb896445c0b016386e28f959667/doc/SDRPager_Interface.png -------------------------------------------------------------------------------- /doc/SDRPager_with_GM1200.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwth-afu/SDRPager/f9c0486dd5f89fb896445c0b016386e28f959667/doc/SDRPager_with_GM1200.JPG -------------------------------------------------------------------------------- /doc/Wiring.SchDoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwth-afu/SDRPager/f9c0486dd5f89fb896445c0b016386e28f959667/doc/Wiring.SchDoc -------------------------------------------------------------------------------- /doc/Wiring_to_Radio.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwth-afu/SDRPager/f9c0486dd5f89fb896445c0b016386e28f959667/doc/Wiring_to_Radio.pdf -------------------------------------------------------------------------------- /raspager-sdr/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | 3 | # Ignore Eclipse project files 4 | .classpath 5 | .project 6 | .settings/ 7 | 8 | # Ignore IntelliJ project files 9 | .idea/ 10 | raspager-sdr.iml -------------------------------------------------------------------------------- /raspager-sdr/config/logging.properties: -------------------------------------------------------------------------------- 1 | handlers = java.util.logging.ConsoleHandler 2 | #handlers = java.util.logging.FileHandler, java.util.logging.ConsoleHandler 3 | 4 | de.rwth_aachen.afu.raspager.level = ALL 5 | 6 | java.util.logging.ConsoleHandler.level = ALL 7 | java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter 8 | 9 | java.util.logging.FileHandler.level = WARNING 10 | java.util.logging.FileHandler.pattern = RasPager.log 11 | java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter 12 | -------------------------------------------------------------------------------- /raspager-sdr/config/raspager.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 01 18:03:40 UTC 2016 2 | invert=true 3 | sdr.device=ALSA [plughw\:0,1] 4 | net.masters=44.225.164.2 5 | gpio.raspirev=RaspberryPi_B_Rev1 6 | sdr.correction=0.5 7 | serial.use=false 8 | net.port=1337 9 | serial.port=/dev/ttyS0 10 | gpio.pin=GPIO 0 11 | gpio.use=true 12 | txDelay=50 13 | -------------------------------------------------------------------------------- /raspager-sdr/config/raspager.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | JAVA=/usr/bin/java 4 | LOG_CONF=logging.properties 5 | OPTS= 6 | 7 | $JAVA -jar -Djava.util.logging.config.file=$LOG_CONF \ 8 | -Djava.library.path=/usr/lib/jni \ 9 | -Djava.net.preferIPv4Stack=true \ 10 | raspager-sdr-2.0.0-SNAPSHOT.jar $OPTS "$@" 11 | -------------------------------------------------------------------------------- /raspager-sdr/config/raspager2.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 01 21:08:42 CET 2016 2 | invert=true 3 | sdr.device=Intel [plughw\:0,0] 4 | net.masters=44.225.164.2 5 | gpio.raspirev=RaspberryPi_B_Rev1 6 | serial.pin=DTR 7 | serial.use=true 8 | sdr.correction=0.22 9 | net.port=1337 10 | serial.port=/dev/ttyUSB0 11 | gpio.use=false 12 | gpio.pin=GPIO 0 13 | txDelay=50 14 | -------------------------------------------------------------------------------- /raspager-sdr/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | de.rwth-aachen.afu 5 | raspager-sdr 6 | 2.0.0-SNAPSHOT 7 | RasPager SDR 8 | 9 | UTF-8 10 | 1.8 11 | 1.8 12 | true 13 | true 14 | 15 | 16 | Amateurfunkgruppe der RWTH Aachen 17 | https://www.afu.rwth-aachen.de/ 18 | 19 | 20 | 21 | io.netty 22 | netty-all 23 | 4.1.8.Final 24 | 25 | 26 | com.pi4j 27 | pi4j-core 28 | 1.1 29 | 30 | 31 | com.pi4j 32 | pi4j-device 33 | 1.1 34 | 35 | 36 | com.pi4j 37 | pi4j-service 38 | 1.1 39 | 40 | 41 | com.pi4j 42 | pi4j-gpio-extension 43 | 1.1 44 | 45 | 46 | org.rxtx 47 | rxtx 48 | 2.1.7 49 | 50 | 51 | commons-cli 52 | commons-cli 53 | 1.3.1 54 | 55 | 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-jar-plugin 61 | 2.4 62 | 63 | 64 | 65 | true 66 | lib 67 | de.rwth_aachen.afu.raspager.Main 68 | true 69 | true 70 | 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-dependency-plugin 77 | 2.8 78 | 79 | 80 | copy-dependencies 81 | prepare-package 82 | 83 | copy-dependencies 84 | 85 | 86 | ${project.build.directory}/lib 87 | false 88 | false 89 | true 90 | 91 | 92 | 93 | 94 | 95 | org.apache.maven.plugins 96 | maven-assembly-plugin 97 | 2.6 98 | 99 | src/assembly/bin.xml 100 | 101 | 102 | 103 | create-archive 104 | package 105 | 106 | single 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | src/main/resources 115 | 116 | 117 | config 118 | ${project.build.directory} 119 | 120 | logging.properties 121 | raspager.properties 122 | raspager.sh 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /raspager-sdr/src/assembly/bin.xml: -------------------------------------------------------------------------------- 1 | 5 | bin 6 | 7 | tar.gz 8 | 9 | false 10 | 11 | 12 | false 13 | lib 14 | false 15 | 16 | 17 | 18 | 19 | ${project.build.directory} 20 | 21 | 22 | *.jar 23 | raspager.properties 24 | logging.properties 25 | raspager.sh 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/ConfigKeys.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | final class ConfigKeys { 4 | 5 | public static final String INVERT = "invert"; 6 | public static final String TX_DELAY = "txDelay"; 7 | public static final String NET_PORT = "net.port"; 8 | public static final String NET_MASTERS = "net.masters"; 9 | public static final String GPIO_USE = "gpio.use"; 10 | public static final String GPIO_PIN = "gpio.pin"; 11 | public static final String GPIO_RASPI_REV = "gpio.raspirev"; 12 | public static final String SERIAL_USE = "serial.use"; 13 | public static final String SERIAL_PORT = "serial.port"; 14 | public static final String SERIAL_PIN = "serial.pin"; 15 | public static final String SDR_DEVICE = "sdr.device"; 16 | public static final String SDR_CORRECTION = "sdr.correction"; 17 | 18 | private ConfigKeys() { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/Configuration.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.FileNotFoundException; 5 | import java.io.FileOutputStream; 6 | import java.io.IOException; 7 | import java.util.NoSuchElementException; 8 | import java.util.Properties; 9 | 10 | /** 11 | * Wrapper class for the main configuration file. 12 | * 13 | * @author Philipp Thiel 14 | */ 15 | public final class Configuration { 16 | private final Properties props = new Properties(); 17 | 18 | /** 19 | * Loads the given configuration file. 20 | * 21 | * @param fileName 22 | * Configuration file to load. 23 | * @throws FileNotFoundException 24 | * If the configuration file does not exist. 25 | * @throws IOException 26 | * If an IO error occurred while reading the configuration file. 27 | */ 28 | public void load(String fileName) throws FileNotFoundException, IOException { 29 | try (FileInputStream fin = new FileInputStream(fileName)) { 30 | props.load(fin); 31 | } 32 | } 33 | 34 | /** 35 | * Saves the current configuration to the given file. 36 | * 37 | * @param fileName 38 | * Name of the configuration file. 39 | * @throws FileNotFoundException 40 | * If the configuration file does not exist. 41 | * @throws IOException 42 | * If an IO error occurred while writing the configuration file. 43 | */ 44 | public void save(String fileName) throws FileNotFoundException, IOException { 45 | try (FileOutputStream fout = new FileOutputStream(fileName)) { 46 | props.store(fout, null); 47 | } 48 | } 49 | 50 | /** 51 | * Checks whether a key exists in the configuration file. 52 | * 53 | * @param key 54 | * Key to look for. 55 | * @return True if the key exists, false otherwise. 56 | */ 57 | public boolean contains(String key) { 58 | return props.containsKey(key); 59 | } 60 | 61 | /** 62 | * Removes a key from the configuration file. 63 | * 64 | * @param key 65 | * Key to remove. 66 | * @return True if the key was removed, false otherwise. 67 | */ 68 | public boolean remove(String key) { 69 | return (props.remove(key) != null); 70 | } 71 | 72 | /** 73 | * Sets a boolean value. 74 | * 75 | * @param key 76 | * Configuration key. 77 | * @param value 78 | * Value to set. 79 | */ 80 | public void setBoolean(String key, boolean value) { 81 | props.setProperty(key, Boolean.toString(value)); 82 | } 83 | 84 | /** 85 | * Gets a boolean from the configuration file. 86 | * 87 | * @param key 88 | * Configuration key. 89 | * @return Value of the configuration key. 90 | * @throws NoSuchElementException 91 | * If the given key does not exist. 92 | */ 93 | public boolean getBoolean(String key) { 94 | String value = props.getProperty(key); 95 | if (value != null) { 96 | return Boolean.parseBoolean(value); 97 | } else { 98 | throw new NoSuchElementException(key); 99 | } 100 | } 101 | 102 | /** 103 | * Gets a boolean from the configuration file or a default value if the key 104 | * does not exist. 105 | * 106 | * @param key 107 | * Configuration key. 108 | * @param defaultValue 109 | * Default value in case the key does not exist. 110 | * @return Value for the configuration key or default value when the key 111 | * does not exist. 112 | */ 113 | public boolean getBoolean(String key, boolean defaultValue) { 114 | String value = props.getProperty(key); 115 | if (value == null) { 116 | return defaultValue; 117 | } else { 118 | return Boolean.parseBoolean(value); 119 | } 120 | } 121 | 122 | /** 123 | * Gets a string from the configuration file. 124 | * 125 | * @param key 126 | * Configuration key. 127 | * @return Value of the configuration key. 128 | * @throws NoSuchElementException 129 | * If the given key does not exist. 130 | */ 131 | public String getString(String key) { 132 | String value = props.getProperty(key); 133 | if (value != null) { 134 | return value; 135 | } else { 136 | throw new NoSuchElementException(key); 137 | } 138 | } 139 | 140 | /** 141 | * Gets a string from the configuration file. 142 | * 143 | * @param key 144 | * Configuration key. 145 | * @param defaultValue 146 | * Default value in case the key does not exist. 147 | * @return Value for the configuration key or default value when the key 148 | * does not exist. 149 | */ 150 | public String getString(String key, String defaultValue) { 151 | return props.getProperty(key, defaultValue); 152 | } 153 | 154 | /** 155 | * Sets a string value. 156 | * 157 | * @param key 158 | * Configuration key. 159 | * @param value 160 | * Value to set. 161 | */ 162 | public void setString(String key, String value) { 163 | // if (value != null) { 164 | props.setProperty(key, value); 165 | // } else { 166 | // remove(key); 167 | // } 168 | } 169 | 170 | /** 171 | * Gets an integer from the configuration file. 172 | * 173 | * @param key 174 | * Configuration key. 175 | * @return Value for the configuration key. 176 | * @throws NoSuchElementException 177 | * If the given key does not exist. 178 | * @throws NumberFormatException 179 | * If the configuration value is not a valid number. 180 | */ 181 | public int getInt(String key) { 182 | String value = props.getProperty(key); 183 | if (value != null) { 184 | return Integer.parseInt(value); 185 | } else { 186 | throw new NoSuchElementException(key); 187 | } 188 | } 189 | 190 | /** 191 | * Gets an integer from the configuration file. 192 | * 193 | * @param key 194 | * Configuration key. 195 | * @param defaultValue 196 | * Default value in case the key does not exist. 197 | * @return Value for the configuration key or default value when the key 198 | * does not exist. 199 | * @throws NumberFormatException 200 | * If the configuration value is not a valid number. 201 | */ 202 | public int getInt(String key, int defaultValue) { 203 | String value = props.getProperty(key); 204 | if (value != null) { 205 | return Integer.parseInt(value); 206 | } else { 207 | return defaultValue; 208 | } 209 | } 210 | 211 | /** 212 | * Sets an integer value. 213 | * 214 | * @param key 215 | * Configuration key. 216 | * @param value 217 | * Value to set. 218 | */ 219 | public void setInt(String key, int value) { 220 | props.setProperty(key, Integer.toString(value)); 221 | } 222 | 223 | /** 224 | * Gets a float from the configuration file. 225 | * 226 | * @param key 227 | * Configuration key. 228 | * @return Value for the configuration key. 229 | * @throws NoSuchElementException 230 | * If the given key does not exist. 231 | * @throws NumberFormatException 232 | * If the configuration value is not a valid number. 233 | */ 234 | public float getFloat(String key) { 235 | String value = props.getProperty(key); 236 | if (value != null) { 237 | return Float.parseFloat(value); 238 | } else { 239 | throw new NoSuchElementException(key); 240 | } 241 | } 242 | 243 | /** 244 | * Gets a float from the configuration file. 245 | * 246 | * @param key 247 | * Configuration key. 248 | * @param defaultValue 249 | * Default value in case the key does not exist. 250 | * @return Value for the configuration key or default value when the key 251 | * does not exist. 252 | * @throws NumberFormatException 253 | * If the configuration value is not a valid number. 254 | */ 255 | public float getFloat(String key, float defaultValue) { 256 | String value = props.getProperty(key); 257 | if (value != null) { 258 | return Float.parseFloat(value); 259 | } else { 260 | return defaultValue; 261 | } 262 | } 263 | 264 | /** 265 | * Sets a float value. 266 | * 267 | * @param key 268 | * Configuration key. 269 | * @param value 270 | * Value to set. 271 | */ 272 | public void setFloat(String key, float value) { 273 | props.setProperty(key, Float.toString(value)); 274 | } 275 | 276 | /** 277 | * Gets a double from the configuration file. 278 | * 279 | * @param key 280 | * Configuration key. 281 | * @return Value for the configuration key. 282 | * @throws NoSuchElementException 283 | * If the given key does not exist. 284 | * @throws NumberFormatException 285 | * If the configuration value is not a valid number. 286 | */ 287 | public double getDouble(String key) { 288 | String value = props.getProperty(key); 289 | if (value != null) { 290 | return Double.parseDouble(value); 291 | } else { 292 | throw new NoSuchElementException(key); 293 | } 294 | } 295 | 296 | /** 297 | * Gets a double from the configuration file. 298 | * 299 | * @param key 300 | * Configuration key. 301 | * @param defaultValue 302 | * Default value in case the key does not exist. 303 | * @return Value for the configuration key or default value when the key 304 | * does not exist. 305 | * @throws NumberFormatException 306 | * If the configuration value is not a valid number. 307 | */ 308 | public double getDouble(String key, double defaultValue) { 309 | String value = props.getProperty(key); 310 | if (value != null) { 311 | return Double.parseDouble(value); 312 | } else { 313 | return defaultValue; 314 | } 315 | } 316 | 317 | /** 318 | * Sets a double value. 319 | * 320 | * @param key 321 | * Configuration key. 322 | * @param value 323 | * Value to set. 324 | */ 325 | public void setDouble(String key, double value) { 326 | props.setProperty(key, Double.toString(value)); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/GpioView.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.awt.BorderLayout; 4 | import java.awt.Toolkit; 5 | 6 | import javax.swing.ImageIcon; 7 | import javax.swing.JFrame; 8 | import javax.swing.JLabel; 9 | import javax.swing.JPanel; 10 | import javax.swing.border.EmptyBorder; 11 | 12 | public class GpioView extends JFrame { 13 | private static final long serialVersionUID = 0; 14 | private JPanel contentPane; 15 | 16 | public GpioView() { 17 | setTitle("Raspberry Pi: GPIO"); 18 | setBounds(100, 100, 500, 907); 19 | contentPane = new JPanel(); 20 | contentPane.setBorder(new EmptyBorder(0, 0, 0, 0)); 21 | contentPane.setLayout(new BorderLayout(0, 0)); 22 | setContentPane(contentPane); 23 | 24 | JLabel label = new JLabel(""); 25 | label.setIcon(new ImageIcon(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/pi_gpio.png")))); 26 | contentPane.add(label, BorderLayout.CENTER); 27 | } 28 | } -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/Main.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | import java.io.PrintStream; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | import org.apache.commons.cli.CommandLine; 11 | import org.apache.commons.cli.CommandLineParser; 12 | import org.apache.commons.cli.DefaultParser; 13 | import org.apache.commons.cli.HelpFormatter; 14 | import org.apache.commons.cli.Options; 15 | import org.apache.commons.cli.ParseException; 16 | 17 | public final class Main { 18 | private static final Logger log = Logger.getLogger(Main.class.getName()); 19 | private static boolean startService = false; 20 | private static boolean withTrayIcon = true; 21 | 22 | private static void initRxTx() { 23 | // Preventing rxtx to write to the console 24 | PrintStream out = System.out; 25 | System.setOut(new PrintStream(new OutputStream() { 26 | @Override 27 | public void write(int b) throws IOException { 28 | } 29 | })); 30 | 31 | try { 32 | Class.forName("gnu.io.RXTXCommDriver"); 33 | } catch (ClassNotFoundException e) { 34 | log.log(Level.SEVERE, "Failed to load RXTX.", e); 35 | } finally { 36 | System.setOut(out); 37 | } 38 | } 39 | 40 | private static void printVersion() { 41 | System.out.println("FunkrufSlave - Version 2.0.0"); 42 | System.out.println("by Ralf Wilke, Michael Delissen und Marvin Menzerath, powered by IHF RWTH Aachen"); 43 | System.out.println("New Versions at https://github.com/dh3wr/SDRPager/releases"); 44 | System.out.println(); 45 | } 46 | 47 | private static boolean parseArguments(String[] args, Configuration config) { 48 | Options opts = new Options(); 49 | opts.addOption("c", "config", true, "Configuration file to use."); 50 | opts.addOption("h", "help", false, "Show this help."); 51 | opts.addOption("v", "version", false, "Show version infomration."); 52 | opts.addOption("s", "service", false, "Run as a service without a GUI."); 53 | opts.addOption("notrayicon", false, "Disable tray icon."); 54 | 55 | CommandLineParser parser = new DefaultParser(); 56 | CommandLine line = null; 57 | try { 58 | line = parser.parse(opts, args); 59 | } catch (ParseException ex) { 60 | log.log(Level.SEVERE, "Failed to parse command line.", ex); 61 | return false; 62 | } 63 | 64 | if (line.hasOption('h')) { 65 | HelpFormatter fmt = new HelpFormatter(); 66 | fmt.printHelp("raspager-sdr", opts); 67 | return false; 68 | } 69 | 70 | if (line.hasOption('v')) { 71 | printVersion(); 72 | return false; 73 | } 74 | 75 | if (line.hasOption('s')) { 76 | startService = true; 77 | } 78 | 79 | if (line.hasOption("notrayicon")) { 80 | withTrayIcon = false; 81 | } 82 | 83 | try { 84 | String configFile = line.getOptionValue('c', "raspager.properties"); 85 | config.load(configFile); 86 | } catch (FileNotFoundException ex) { 87 | log.log(Level.WARNING, "Failed to load configuration: {0}", ex.getMessage()); 88 | } catch (Throwable t) { 89 | log.log(Level.SEVERE, "Failed to load configuration.", t); 90 | } 91 | 92 | return true; 93 | } 94 | 95 | public static void main(String[] args) { 96 | Thread.setDefaultUncaughtExceptionHandler((t, e) -> { 97 | log.log(Level.SEVERE, String.format("Uncaught exception in thread %s.", t.getName()), e); 98 | }); 99 | 100 | Configuration config = new Configuration(); 101 | if (!parseArguments(args, config)) { 102 | return; 103 | } 104 | 105 | initRxTx(); 106 | 107 | RasPagerService app = null; 108 | try { 109 | app = new RasPagerService(config, startService, withTrayIcon); 110 | app.run(); 111 | } catch (Throwable t) { 112 | log.log(Level.SEVERE, "Main application error.", t); 113 | } finally { 114 | if (app != null) { 115 | app.shutdown(); 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/MasterServerFilter.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.util.logging.Level; 5 | import java.util.logging.Logger; 6 | 7 | import io.netty.channel.ChannelFuture; 8 | import io.netty.channel.ChannelHandler.Sharable; 9 | import io.netty.channel.ChannelHandlerContext; 10 | import io.netty.handler.ipfilter.AbstractRemoteAddressFilter; 11 | 12 | /** 13 | * This class implements an IP-based filter for master servers. 14 | * 15 | * @author Philipp Thiel 16 | */ 17 | @Sharable 18 | final class MasterServerFilter extends AbstractRemoteAddressFilter { 19 | private static final Logger log = Logger.getLogger(MasterServerFilter.class.getName()); 20 | private final String[] masters; 21 | 22 | /** 23 | * Creates a new filter instance. 24 | * 25 | * @param masters 26 | * IP addresses of valid master servers. 27 | */ 28 | public MasterServerFilter(String... masters) { 29 | if (masters != null) { 30 | this.masters = masters; 31 | } else { 32 | throw new NullPointerException("masters"); 33 | } 34 | } 35 | 36 | @Override 37 | protected boolean accept(ChannelHandlerContext ctx, InetSocketAddress remoteAddress) throws Exception { 38 | String addr = remoteAddress.getAddress().getHostAddress(); 39 | for (String m : masters) { 40 | if (m.equalsIgnoreCase(addr)) { 41 | return true; 42 | } 43 | } 44 | 45 | return false; 46 | } 47 | 48 | @Override 49 | protected ChannelFuture channelRejected(ChannelHandlerContext ctx, InetSocketAddress remoteAddress) { 50 | log.log(Level.WARNING, "Connection rejected: {0}", remoteAddress.getHostString()); 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/Message.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.util.List; 4 | 5 | final class Message { 6 | private final int type; 7 | private final int speed; 8 | private final int address; 9 | private final int function; 10 | private final String text; 11 | private final List codeWords; // 0 = framePos, 1 = cw, 2 = cw, ... 12 | 13 | public Message(String str) { 14 | this(str.split(":", 5)); 15 | } 16 | 17 | public Message(String[] parts) { 18 | if (parts.length < 5) { 19 | throw new IllegalArgumentException("Invalid sized array."); 20 | } 21 | 22 | type = parts[0].charAt(4) - '0'; 23 | speed = Integer.parseInt(parts[1]); 24 | address = Integer.parseInt(parts[2], 16); 25 | function = Integer.parseInt(parts[3]); 26 | text = parts[4]; 27 | 28 | switch (type) { 29 | case 5: 30 | // numeric 31 | // #00 5:1:9C8:0:094016 130412 32 | codeWords = Pocsag.encodeNumber(address, function, text); 33 | break; 34 | case 6: 35 | // alpha numeric 36 | codeWords = Pocsag.encodeText(address, function, text); 37 | break; 38 | default: 39 | throw new IllegalArgumentException("Invalid message type: " + type); 40 | } 41 | } 42 | 43 | public int getType() { 44 | return type; 45 | } 46 | 47 | public int getSpeed() { 48 | return speed; 49 | } 50 | 51 | public long getAddress() { 52 | return address; 53 | } 54 | 55 | public int getFunction() { 56 | return function; 57 | } 58 | 59 | public String getText() { 60 | return text; 61 | } 62 | 63 | public List getCodeWords() { 64 | return codeWords; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/Pocsag.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | final class Pocsag { 7 | private static final char[] isotab = { 0x00, 0x40, 0x20, 0x60, 0x10, 0x50, 0x30, 0x70, 0x08, 0x48, 0x28, 0x68, 0x18, 8 | 0x58, 0x38, 0x78, 0x04, 0x44, 0x24, 0x64, 0x14, 0x54, 0x34, 0x74, 0x0c, 0x4c, 0x2c, 0x6c, 0x1c, 0x5c, 0x3c, 9 | 0x7c, 0x02, 0x42, 0x22, 0x62, 0x12, 0x52, 0x32, 0x72, 0x0a, 0x4a, 0x2a, 0x6a, 0x1a, 0x5a, 0x3a, 0x7a, 0x06, 10 | 0x46, 0x26, 0x66, 0x16, 0x56, 0x36, 0x76, 0x0e, 0x4e, 0x2e, 0x6e, 0x1e, 0x5e, 0x3e, 0x7e, 0x01, 0x41, 0x21, 11 | 0x61, 0x11, 0x51, 0x31, 0x71, 0x09, 0x49, 0x29, 0x69, 0x19, 0x59, 0x39, 0x79, 0x05, 0x45, 0x25, 0x65, 0x15, 12 | 0x55, 0x35, 0x75, 0x0d, 0x4d, 0x2d, 0x6d, 0x1d, 0x5d, 0x3d, 0x7d, 0x03, 0x43, 0x23, 0x63, 0x13, 0x53, 0x33, 13 | 0x73, 0x0b, 0x4b, 0x2b, 0x6b, 0x1b, 0x5b, 0x3b, 0x7b, 0x07, 0x47, 0x27, 0x67, 0x17, 0x57, 0x37, 0x77, 0x0f, 14 | 0x4f, 0x2f, 0x6f, 0x1f, 0x5f, 0x3f, 0x7f, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 15 | 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 16 | 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x01, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 17 | 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 18 | 0x3a, 0x3a, 0x3a, 0x6d, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 19 | 0x3a, 0x3a, 0x3a, 0x1d, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x5d, 0x3a, 0x3a, 0x3f, 0x3a, 0x3a, 0x3a, 0x3a, 0x6f, 20 | 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x1f, 21 | 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x5f, 0x3a, 0x3a, 0x3a }; 22 | 23 | // settings 24 | public static final int POC_BITS_PER_CW = 20; 25 | public static final int POC_BITS_PER_CHAR = 7; 26 | public static final int POC_BITS_PER_DIGIT = 4; 27 | 28 | // special code words 29 | public static final int PRAEAMBLE = 0xaaaaaaaa; // send 18 times // 0xaa = 30 | // 0b10101010 4 31 | public static final int SYNC = 0x7CD215D8; // sync-codeword 32 | public static final int IDLE = 0x7A89C197; // idle-codeword 33 | 34 | public static int crc(int cw) { 35 | int crc; 36 | int d; 37 | int m; 38 | char p; 39 | 40 | // crc 41 | crc = cw; 42 | d = 0xed200000; 43 | 44 | for (m = 0x80000000; (m & 0x400) == 0; m >>>= 1) { 45 | // m ist Bitmaske mit nur einer 1, die vom MSB bis vor den Anfang 46 | // des (CRC+Praität) bereichs läuft, d.h. bis Bit 11 einschl. 47 | if ((crc & m) != 0) 48 | crc ^= d; 49 | 50 | d >>>= 1; 51 | } 52 | 53 | cw |= crc; 54 | 55 | // parity 56 | 57 | p = (char) (((cw >>> 24) & 0xff) ^ ((cw >>> 16) & 0xff) ^ ((cw >>> 8) & 0xff) ^ (cw & 0xff)); 58 | 59 | p ^= (p >>> 4); 60 | p ^= (p >>> 2); 61 | p ^= (p >>> 1); 62 | p &= 0x01; 63 | 64 | return cw | p; 65 | } 66 | 67 | public static int encodeACW(int addr, int func) { 68 | 69 | return (((addr & 0x001ffff8) << 10) | ((func & 0x00000003) << 11)); 70 | 71 | } 72 | 73 | public static int encodeMCW(int msg) { 74 | 75 | return (0x80000000 | ((msg & 0x000fffff) << 11)); 76 | 77 | } 78 | 79 | public static char encodeChar(char ch) { 80 | return isotab[ch & 0xff]; 81 | } 82 | 83 | public static char encodeDigit(char ch) { 84 | char[] mirrorTab = { 0x00, 0x08, 0x04, 0x0c, 0x02, 0x0a, 0x06, 0x0e, 0x01, 0x09 }; 85 | 86 | if (ch >= '0' && ch <= '9') 87 | return mirrorTab[ch - '0']; 88 | 89 | switch (ch) { 90 | case ' ': 91 | return 0x03; 92 | 93 | case 'U': 94 | return 0x0d; 95 | 96 | case '_': 97 | return 0x0b; 98 | 99 | case '[': 100 | return 0x0f; 101 | 102 | case ']': 103 | return 0x07; 104 | } 105 | 106 | return 0x05; 107 | } 108 | 109 | public static List encodeNumber(int addr, int func, String text) { 110 | List cwBuf = new ArrayList<>(); 111 | int msg = 0; 112 | int msgBitsLeft; 113 | 114 | int framePos = addr & 7; 115 | cwBuf.add(framePos); // position 0 116 | 117 | // Adress-Codewort erzeugen und im Puffer speichern. 118 | cwBuf.add(crc(encodeACW(addr, func))); 119 | 120 | // Komplette Nachricht codieren und speichern. 121 | msgBitsLeft = POC_BITS_PER_CW; 122 | 123 | for (int i = 0; i < text.length(); i++) { 124 | char ch = encodeDigit(text.charAt(i)); 125 | 126 | msg <<= POC_BITS_PER_DIGIT; 127 | msg |= ch; 128 | msgBitsLeft -= POC_BITS_PER_DIGIT; 129 | 130 | if (msgBitsLeft == 0) { 131 | cwBuf.add(crc(encodeMCW(msg))); 132 | msgBitsLeft = POC_BITS_PER_CW; 133 | } 134 | } 135 | 136 | // Wenn das letzte Codewort nicht komplett ist, wird es mit Spaces 137 | // aufgefuellt. 138 | if (msgBitsLeft != POC_BITS_PER_CW) { 139 | while (msgBitsLeft > 0) { 140 | msg <<= POC_BITS_PER_DIGIT; 141 | msg |= 0x03; /* Space */ 142 | msgBitsLeft -= POC_BITS_PER_DIGIT; 143 | } 144 | 145 | cwBuf.add(crc(encodeMCW(msg))); 146 | } 147 | 148 | return cwBuf; 149 | } 150 | 151 | public static List encodeText(int addr, int func, String text) { 152 | List cwBuf = new ArrayList<>(); 153 | int msg = 0; 154 | int msgBitsLeft; 155 | 156 | int framePos = addr & 7; 157 | cwBuf.add(framePos); // position 0 158 | 159 | // Adress-Codewort erzeugen und im Puffer speichern. 160 | cwBuf.add(crc(encodeACW(addr, func))); 161 | 162 | // Komplette Nachricht codieren und speichern. 163 | msgBitsLeft = POC_BITS_PER_CW; 164 | 165 | for (int i = 0; i < text.length(); i++) { 166 | char ch = encodeChar(text.charAt(i)); 167 | 168 | if (msgBitsLeft >= POC_BITS_PER_CHAR) { 169 | msg <<= POC_BITS_PER_CHAR; 170 | msg |= ch; 171 | msgBitsLeft -= POC_BITS_PER_CHAR; 172 | 173 | if (msgBitsLeft == 0) { 174 | cwBuf.add(crc(encodeMCW(msg))); 175 | msgBitsLeft = POC_BITS_PER_CW; 176 | } 177 | } else { 178 | msg <<= msgBitsLeft; 179 | msg |= ch >> (POC_BITS_PER_CHAR - msgBitsLeft); 180 | 181 | cwBuf.add(crc(encodeMCW(msg))); 182 | 183 | msg = ch; 184 | msgBitsLeft = POC_BITS_PER_CW - POC_BITS_PER_CHAR + msgBitsLeft; 185 | } 186 | } 187 | 188 | if (msgBitsLeft != POC_BITS_PER_CW) { 189 | msg <<= msgBitsLeft; 190 | cwBuf.add(crc(encodeMCW(msg))); 191 | } 192 | 193 | return cwBuf; 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/RasPagerService.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.IOException; 5 | import java.util.Deque; 6 | import java.util.Timer; 7 | import java.util.concurrent.ConcurrentLinkedDeque; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | 11 | import de.rwth_aachen.afu.raspager.sdr.SDRTransmitter; 12 | 13 | final class RasPagerService { 14 | private static final Logger log = Logger.getLogger(RasPagerService.class.getName()); 15 | 16 | private static final float DEFAULT_SEARCH_STEP_SIZE = 0.05f; 17 | private float searchStepSize = DEFAULT_SEARCH_STEP_SIZE; 18 | private ThreadWrapper server; 19 | private boolean running = false; 20 | 21 | private Timer timer = new Timer(); 22 | private final Deque messages = new ConcurrentLinkedDeque<>(); 23 | private final SDRTransmitter transmitter = new SDRTransmitter(); 24 | private final Configuration config; 25 | private final RasPagerWindow window; 26 | private Scheduler scheduler; 27 | 28 | public RasPagerService(Configuration config, boolean startService, boolean withTrayIcon) 29 | throws FileNotFoundException, IOException { 30 | this.config = config; 31 | 32 | if (!startService) { 33 | window = new RasPagerWindow(this, withTrayIcon); 34 | } else { 35 | window = null; 36 | } 37 | } 38 | 39 | public Configuration getConfig() { 40 | return config; 41 | } 42 | 43 | public boolean isRunning() { 44 | return running; 45 | } 46 | 47 | public boolean isServerRunning() { 48 | return server != null; 49 | } 50 | 51 | public SDRTransmitter getTransmitter() { 52 | return transmitter; 53 | } 54 | 55 | public float getSearchStepSize() { 56 | return searchStepSize; 57 | } 58 | 59 | public void startScheduler(boolean searching) { 60 | try { 61 | transmitter.init(config); 62 | } catch (Exception ex) { 63 | log.log(Level.SEVERE, "Failed to init transmitter.", ex); 64 | 65 | String msg = ex.getMessage(); 66 | if (msg == null || msg.isEmpty()) { 67 | msg = ex.getClass().getName(); 68 | } 69 | 70 | if (window != null) { 71 | window.showError("Failed to init transmitter", msg); 72 | } 73 | 74 | return; 75 | } 76 | 77 | int period = 100; 78 | if (searching) { 79 | scheduler = new SearchScheduler(this, messages); 80 | period = 5000; 81 | } else { 82 | scheduler = new Scheduler(config, messages, transmitter); 83 | } 84 | 85 | if (window != null) { 86 | scheduler.setUpdateTimeSlotsHandler(window::updateTimeSlots); 87 | } 88 | 89 | if (server != null) { 90 | server.getJob().setGetTimeHandler(scheduler::getTime); 91 | server.getJob().setTimeCorrectionHandler(scheduler::correctTime); 92 | server.getJob().setTimeSlotsHandler(scheduler::setTimeSlots); 93 | 94 | if (window != null) { 95 | server.getJob().setConnectionHandler(() -> { 96 | window.setStatus(true); 97 | }); 98 | 99 | server.getJob().setDisconnectHandler(() -> { 100 | window.setStatus(false); 101 | }); 102 | } 103 | } 104 | 105 | timer = new Timer(); 106 | timer.schedule(scheduler, 100, period); 107 | } 108 | 109 | public void stopScheduler() { 110 | if (scheduler != null) { 111 | scheduler.cancel(); 112 | scheduler = null; 113 | } 114 | 115 | try { 116 | transmitter.close(); 117 | } catch (Exception e) { 118 | log.log(Level.SEVERE, "Failed to close transmitter.", e); 119 | } 120 | } 121 | 122 | public void startServer(boolean join) { 123 | if (server == null) { 124 | int port = config.getInt(ConfigKeys.NET_PORT, 1337); 125 | String[] masters = null; 126 | if (config.contains(ConfigKeys.NET_MASTERS)) { 127 | String v = config.getString(ConfigKeys.NET_MASTERS); 128 | masters = v.split(" +"); 129 | } 130 | 131 | Server srv = new Server(port, masters); 132 | // Register event handlers 133 | srv.setAddMessageHandler(messages::push); 134 | // Create new server thread 135 | server = new ThreadWrapper(srv); 136 | } 137 | 138 | // start scheduler (not searching) 139 | startScheduler(false); 140 | 141 | server.start(); 142 | 143 | running = true; 144 | log.info("Server is running."); 145 | 146 | if (join) { 147 | try { 148 | server.join(); 149 | } catch (InterruptedException e) { 150 | log.log(Level.SEVERE, "Server thread interrupted.", e); 151 | } 152 | 153 | stopServer(true); 154 | } 155 | 156 | if (window != null) { 157 | window.setStatus(false); 158 | } 159 | } 160 | 161 | public void stopServer(boolean error) { 162 | log.info("Server is shutting down."); 163 | 164 | // if there was no error, halt server 165 | if (server != null) { 166 | server.getJob().shutdown(); 167 | } 168 | 169 | server = null; 170 | 171 | // set running to false 172 | running = false; 173 | 174 | // stop scheduler 175 | stopScheduler(); 176 | 177 | messages.clear(); 178 | 179 | log.info("Server stopped."); 180 | 181 | if (window != null) { 182 | window.resetButtons(); 183 | } 184 | } 185 | 186 | public void serverError(String message) { 187 | // set running to false 188 | running = false; 189 | 190 | // stop scheduler 191 | stopScheduler(); 192 | 193 | server = null; 194 | 195 | if (window != null) { 196 | window.showError("Server Error", message); 197 | window.resetButtons(); 198 | } 199 | } 200 | 201 | public void stopSearching() { 202 | if (window != null) { 203 | window.runSearch(false); 204 | } 205 | } 206 | 207 | public float getStepSize() { 208 | float stepWidth = searchStepSize; 209 | 210 | if (window != null) { 211 | String s = window.getStepWidth(); 212 | 213 | if (!s.isEmpty()) { 214 | try { 215 | stepWidth = Float.parseFloat(s); 216 | } catch (NumberFormatException e) { 217 | log.log(Level.SEVERE, "Invalid step size.", e); 218 | } 219 | } 220 | } 221 | 222 | return stepWidth; 223 | } 224 | 225 | public void run() { 226 | if (window == null) { 227 | startServer(true); 228 | } 229 | } 230 | 231 | public void shutdown() { 232 | try { 233 | if (transmitter != null) { 234 | transmitter.close(); 235 | } 236 | } catch (Throwable t) { 237 | log.log(Level.SEVERE, "Failed to close transmitter.", t); 238 | } 239 | 240 | timer.cancel(); 241 | } 242 | 243 | public RasPagerWindow getWindow() { 244 | // TODO replace 245 | return window; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/RasPagerWindow.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.awt.AWTException; 4 | import java.awt.BorderLayout; 5 | import java.awt.Canvas; 6 | import java.awt.Color; 7 | import java.awt.Dimension; 8 | import java.awt.EventQueue; 9 | import java.awt.Font; 10 | import java.awt.Frame; 11 | import java.awt.Graphics; 12 | import java.awt.Image; 13 | import java.awt.List; 14 | import java.awt.MenuItem; 15 | import java.awt.PopupMenu; 16 | import java.awt.Rectangle; 17 | import java.awt.SystemTray; 18 | import java.awt.Toolkit; 19 | import java.awt.TrayIcon; 20 | import java.awt.event.KeyEvent; 21 | import java.awt.event.KeyListener; 22 | import java.awt.event.WindowEvent; 23 | import java.awt.event.WindowListener; 24 | import java.io.File; 25 | import java.util.Arrays; 26 | import java.util.ResourceBundle; 27 | import java.util.logging.Level; 28 | import java.util.logging.Logger; 29 | 30 | import javax.sound.sampled.AudioSystem; 31 | import javax.sound.sampled.Mixer; 32 | import javax.swing.JButton; 33 | import javax.swing.JCheckBox; 34 | import javax.swing.JComboBox; 35 | import javax.swing.JFileChooser; 36 | import javax.swing.JFrame; 37 | import javax.swing.JLabel; 38 | import javax.swing.JOptionPane; 39 | import javax.swing.JPanel; 40 | import javax.swing.JRadioButton; 41 | import javax.swing.JScrollPane; 42 | import javax.swing.JSlider; 43 | import javax.swing.JTextField; 44 | import javax.swing.SwingConstants; 45 | import javax.swing.WindowConstants; 46 | import javax.swing.border.TitledBorder; 47 | 48 | import com.pi4j.io.gpio.Pin; 49 | import com.pi4j.io.gpio.RaspiPin; 50 | import com.pi4j.system.SystemInfo.BoardType; 51 | 52 | import de.rwth_aachen.afu.raspager.sdr.SerialPortComm; 53 | 54 | public class RasPagerWindow extends JFrame { 55 | private static final Logger log = Logger.getLogger(RasPagerWindow.class.getName()); 56 | private static final long serialVersionUID = 1L; 57 | 58 | private JPanel main; 59 | private final int WIDTH = 633; 60 | private final int HEIGHT = 450; 61 | 62 | private TrayIcon trayIcon; 63 | 64 | private JLabel correctionActual; 65 | private JSlider correctionSlider; 66 | private List masterList; 67 | private JLabel statusDisplay; 68 | private JTextField searchStepWidth; 69 | private JButton startButton; 70 | private JTextField masterIP; 71 | private JTextField port; 72 | private Canvas slotDisplay; 73 | private JPanel serialPanel; 74 | private JPanel gpioPanel; 75 | private JComboBox serialPortList; 76 | private JComboBox serialPin; 77 | private JCheckBox invert; 78 | private JTextField delay; 79 | private JComboBox raspiList; 80 | private JComboBox gpioList; 81 | private JButton btnGpioPins; 82 | private JRadioButton radioUseSerial; 83 | private JRadioButton radioUseGpio; 84 | private JComboBox soundDeviceList; 85 | 86 | private JButton searchStart; 87 | private JButton searchStop; 88 | private JTextField searchAddress; 89 | 90 | private final RasPagerService app; 91 | // private final Configuration config; 92 | // private final SDRTransmitter transmitter; 93 | private TimeSlots timeSlots = new TimeSlots(); 94 | private final ResourceBundle texts; 95 | 96 | // constructor 97 | public RasPagerWindow(RasPagerService app, boolean withTrayIcon) { 98 | this.app = app; 99 | 100 | // Load locale stuff 101 | texts = ResourceBundle.getBundle("MainWindow"); 102 | 103 | // set window preferences 104 | setTitle("FunkrufSlave"); 105 | setResizable(false); 106 | setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 107 | 108 | // window listener 109 | addWindowListener(new WindowListener() { 110 | @Override 111 | public void windowActivated(WindowEvent arg0) { 112 | } 113 | 114 | @Override 115 | public void windowClosed(WindowEvent arg0) { 116 | System.exit(0); 117 | } 118 | 119 | @Override 120 | public void windowClosing(WindowEvent event) { 121 | if (app.isRunning() && !showConfirmResource("askQuitTitle", "askQuitText")) { 122 | return; 123 | } 124 | 125 | if (app.isRunning()) { 126 | app.stopServer(false); 127 | } 128 | 129 | dispose(); 130 | } 131 | 132 | @Override 133 | public void windowDeactivated(WindowEvent arg0) { 134 | } 135 | 136 | @Override 137 | public void windowDeiconified(WindowEvent arg0) { 138 | setVisible(true); 139 | } 140 | 141 | @Override 142 | public void windowIconified(WindowEvent arg0) { 143 | setVisible(false); 144 | } 145 | 146 | @Override 147 | public void windowOpened(WindowEvent arg0) { 148 | } 149 | }); 150 | 151 | // main panel 152 | main = new JPanel(null); 153 | main.setPreferredSize(new Dimension(840, 470)); 154 | main.setBounds(0, 0, WIDTH, HEIGHT); 155 | getContentPane().add(main, BorderLayout.SOUTH); 156 | 157 | // correction slider bounds 158 | Rectangle correctionSliderBounds = new Rectangle(100, 68, 30, 260); 159 | 160 | // correction slider label 161 | JLabel correctionLabel = new JLabel(texts.getString("correctionLabel")); 162 | correctionLabel.setBounds(correctionSliderBounds.x - 20, correctionSliderBounds.y - 38, 80, 18); 163 | main.add(correctionLabel); 164 | 165 | // correction slider actual position 166 | correctionActual = new JLabel("0.00"); 167 | correctionActual.setHorizontalAlignment(SwingConstants.CENTER); 168 | correctionActual.setBounds(correctionSliderBounds.x - 20, correctionSliderBounds.y - 20, 68, 18); 169 | main.add(correctionActual); 170 | 171 | // correction slider position 1 172 | JLabel correctionPosition0 = new JLabel("1"); 173 | correctionPosition0.setBounds(correctionSliderBounds.x - 20, correctionSliderBounds.y, 18, 18); 174 | correctionPosition0.setHorizontalAlignment(SwingConstants.RIGHT); 175 | main.add(correctionPosition0); 176 | 177 | // correction slider position 0 178 | JLabel correctionPosition1 = new JLabel("0"); 179 | correctionPosition1.setBounds(correctionSliderBounds.x - 20, 180 | correctionSliderBounds.y + correctionSliderBounds.height / 2 - 9, 18, 18); 181 | correctionPosition1.setHorizontalAlignment(SwingConstants.RIGHT); 182 | main.add(correctionPosition1); 183 | 184 | // correction slider position -1 185 | JLabel correctionPosition2 = new JLabel("-1"); 186 | correctionPosition2.setBounds(correctionSliderBounds.x - 20, 187 | correctionSliderBounds.y + correctionSliderBounds.height - 18, 18, 18); 188 | correctionPosition2.setHorizontalAlignment(SwingConstants.RIGHT); 189 | main.add(correctionPosition2); 190 | 191 | // correction slider 192 | correctionSlider = new JSlider(SwingConstants.VERTICAL, -100, 100, 0); 193 | correctionSlider.setBounds(correctionSliderBounds); 194 | correctionSlider.addChangeListener((e) -> { 195 | correctionActual.setText(String.format("%+5.2f", correctionSlider.getValue() / 100.)); 196 | app.getTransmitter().setCorrection(correctionSlider.getValue() / 100.0f); 197 | }); 198 | main.add(correctionSlider); 199 | 200 | // search run label 201 | JLabel searchLabel = new JLabel(texts.getString("searchLabel")); 202 | searchLabel.setBounds(200, 414, 100, 18); 203 | main.add(searchLabel); 204 | 205 | // search run start 206 | searchStart = new JButton(texts.getString("searchStart")); 207 | searchStart.setBounds(200, 434, 70, 18); 208 | searchStart.addActionListener((e) -> { 209 | runSearch(true); 210 | }); 211 | main.add(searchStart); 212 | 213 | // search run stop 214 | searchStop = new JButton(texts.getString("searchStop")); 215 | searchStop.setBounds(275, 434, 70, 18); 216 | searchStop.setEnabled(false); 217 | searchStop.addActionListener((e) -> { 218 | app.stopSearching(); 219 | }); 220 | main.add(searchStop); 221 | 222 | // search run step label 223 | JLabel searchStepLabel = new JLabel(texts.getString("searchStepLabel")); 224 | searchStepLabel.setBounds(350, 414, 100, 18); 225 | main.add(searchStepLabel); 226 | 227 | // search run step 228 | searchStepWidth = new JTextField(); 229 | searchStepWidth.setBounds(new Rectangle(350, 434, 80, 18)); 230 | searchStepWidth.addKeyListener(new KeyListener() { 231 | @Override 232 | public void keyTyped(KeyEvent event) { 233 | char key = event.getKeyChar(); 234 | if ((key > '9' || key < '0') && key != '.') { 235 | event.consume(); 236 | } 237 | } 238 | 239 | @Override 240 | public void keyReleased(KeyEvent arg0) { 241 | } 242 | 243 | @Override 244 | public void keyPressed(KeyEvent arg0) { 245 | } 246 | }); 247 | main.add(searchStepWidth); 248 | 249 | // search address label 250 | JLabel searchAddressLabel = new JLabel(texts.getString("searchAddressLabel")); 251 | searchAddressLabel.setBounds(455, 414, 120, 18); 252 | main.add(searchAddressLabel); 253 | 254 | // search address 255 | searchAddress = new JTextField(); 256 | searchAddress.setBounds(455, 434, 100, 18); 257 | searchAddress.addKeyListener(new KeyListener() { 258 | @Override 259 | public void keyTyped(KeyEvent event) { 260 | char key = event.getKeyChar(); 261 | if ((key > '9' || key < '0')) { 262 | event.consume(); 263 | } 264 | } 265 | 266 | @Override 267 | public void keyReleased(KeyEvent arg0) { 268 | } 269 | 270 | @Override 271 | public void keyPressed(KeyEvent arg0) { 272 | } 273 | }); 274 | main.add(searchAddress); 275 | 276 | // slot display bounds 277 | Rectangle slotDisplayBounds = new Rectangle(10, 68, 30, 260); 278 | 279 | // slot display label 280 | JLabel slotDisplayLabel = new JLabel(texts.getString("slotDisplayLabel")); 281 | slotDisplayLabel.setBounds(slotDisplayBounds.x - 2, slotDisplayBounds.y - 38, 50, 18); 282 | main.add(slotDisplayLabel); 283 | 284 | // slot display 285 | slotDisplay = new Canvas() { 286 | private static final long serialVersionUID = 1L; 287 | 288 | @Override 289 | public void paint(Graphics g) { 290 | super.paint(g); 291 | int width = getWidth() - 1; 292 | int height = getHeight() - 1; 293 | int x = 15; 294 | 295 | // draw border 296 | g.drawRect(x, 0, width - x, height); 297 | 298 | int step = getHeight() / 16; 299 | 300 | for (int i = 0, y = step; i < 16; y += step, i++) { 301 | 302 | Font font = g.getFont(); 303 | Color color = g.getColor(); 304 | 305 | // if this is allowed slot 306 | if (timeSlots.get(i)) { 307 | // change font and color 308 | g.setFont(new Font(font.getFontName(), Font.BOLD, font.getSize())); 309 | g.setColor(Color.green); 310 | } 311 | 312 | g.drawString("" + Integer.toHexString(i).toUpperCase(), 0, y); 313 | g.setFont(font); 314 | g.setColor(color); 315 | 316 | // draw line 317 | if (i < 16 - 1) { 318 | g.drawLine(x, y, width, y); 319 | } 320 | 321 | } 322 | 323 | // if scheduler does not exist, function ends here 324 | // TODO fix 325 | // if (state.scheduler == null) { 326 | // return; 327 | // } 328 | return; 329 | 330 | // Color color = g.getColor(); 331 | // g.setColor(Color.green); 332 | // 333 | // // get slot count 334 | // int slot = TimeSlots.getSlotIndex(state.scheduler.getTime()); 335 | // int slotCount = timeSlots.getSlotCount(String.format("%1x", 336 | // slot).charAt(0)); 337 | // 338 | // // draw current slots (from slot to slot + slotCount) with 339 | // // different color 340 | // for (int i = 0; i < slotCount; i++) { 341 | // g.fillRect(x + 1, (slot + i) * step + 1, width - x - 1, step 342 | // - 1); 343 | // } 344 | // 345 | // g.setColor(Color.yellow); 346 | // 347 | // g.fillRect(x + 1, slot * step + 1, width - x - 1, step - 1); 348 | // 349 | // g.setColor(color); 350 | } 351 | }; 352 | slotDisplay.setBounds(slotDisplayBounds); 353 | main.add(slotDisplay); 354 | 355 | // status display label 356 | JLabel statusDisplayLabel = new JLabel(texts.getString("statusDisplayLabel")); 357 | statusDisplayLabel.setBounds(200, 10, 60, 18); 358 | main.add(statusDisplayLabel); 359 | 360 | // status display 361 | statusDisplay = new JLabel(texts.getString("statusDisplayDis")); 362 | statusDisplay.setBounds(new Rectangle(263, 10, 120, 18)); 363 | main.add(statusDisplay); 364 | 365 | // server start button 366 | startButton = new JButton(texts.getString("startButtonStart")); 367 | startButton.addActionListener((e) -> { 368 | if (app.isRunning()) { 369 | app.stopServer(false); 370 | startButton.setText(texts.getString("startButtonStart")); 371 | 372 | } else { 373 | app.startServer(false); 374 | startButton.setText(texts.getString("startButtonStop")); 375 | } 376 | }); 377 | startButton.setBounds(new Rectangle(675, 10, 150, 18)); 378 | main.add(startButton); 379 | 380 | // configuration panel 381 | JPanel configurationPanel = new JPanel(null); 382 | configurationPanel.setBorder(new TitledBorder(null, texts.getString("configurationPanel"), TitledBorder.LEADING, 383 | TitledBorder.TOP, null, null)); 384 | configurationPanel.setBounds(new Rectangle(200, 30, 625, 372)); 385 | main.add(configurationPanel); 386 | 387 | // master list bounds 388 | Rectangle masterListBounds = new Rectangle(0, 30, 150, 200); 389 | 390 | // master list label 391 | JLabel masterListLabel = new JLabel(texts.getString("masterListLabel")); 392 | masterListLabel.setBounds(12, 20, 70, 18); 393 | configurationPanel.add(masterListLabel); 394 | 395 | // master list 396 | masterList = new List(); 397 | masterList.setName("masterList"); 398 | 399 | // master list pane 400 | JScrollPane masterListPane = new JScrollPane(masterList); 401 | masterListPane.setBounds(new Rectangle(12, 38, 150, 218)); 402 | configurationPanel.add(masterListPane); 403 | 404 | // serial delay label 405 | JLabel serialDelayLabel = new JLabel(texts.getString("serialDelayLabel")); 406 | serialDelayLabel.setBounds(174, 292, 50, 18); 407 | configurationPanel.add(serialDelayLabel); 408 | 409 | // serial delay 410 | delay = new JTextField(); 411 | delay.addKeyListener(new KeyListener() { 412 | 413 | @Override 414 | public void keyTyped(KeyEvent event) { 415 | char key = event.getKeyChar(); 416 | 417 | // check if key is between 0 and 9 418 | if (key > '9' || key < '0') { 419 | event.consume(); 420 | } 421 | } 422 | 423 | @Override 424 | public void keyReleased(KeyEvent arg0) { 425 | } 426 | 427 | @Override 428 | public void keyPressed(KeyEvent arg0) { 429 | } 430 | }); 431 | delay.setBounds(265, 292, 50, 18); 432 | configurationPanel.add(delay); 433 | 434 | // serial delay ms 435 | JLabel serialDelayMs = new JLabel("ms"); 436 | serialDelayMs.setBounds(317, 292, 40, 18); 437 | configurationPanel.add(serialDelayMs); 438 | 439 | // port bounds 440 | Rectangle portBounds = new Rectangle(50, masterListBounds.y + masterListBounds.height + 15, 50, 18); 441 | 442 | // port label 443 | JLabel portLabel = new JLabel("Port:"); 444 | portLabel.setBounds(12, 268, 50, 18); 445 | configurationPanel.add(portLabel); 446 | 447 | // port 448 | port = new JTextField(); 449 | port.setBounds(new Rectangle(50, 268, 50, 18)); 450 | port.addKeyListener(new KeyListener() { 451 | 452 | @Override 453 | public void keyTyped(KeyEvent event) { 454 | char key = event.getKeyChar(); 455 | 456 | // check if key is between 0 and 9 457 | if (key > '9' || key < '0') { 458 | event.consume(); 459 | } 460 | 461 | } 462 | 463 | @Override 464 | public void keyReleased(KeyEvent arg0) { 465 | } 466 | 467 | @Override 468 | public void keyPressed(KeyEvent arg0) { 469 | } 470 | }); 471 | configurationPanel.add(port); 472 | 473 | // sounddevice 474 | JLabel soundDeviceLabel = new JLabel(texts.getString("soundDeviceLabel")); 475 | soundDeviceLabel.setBounds(174, 318, 100, 15); 476 | configurationPanel.add(soundDeviceLabel); 477 | 478 | soundDeviceList = new JComboBox<>(); 479 | soundDeviceList.setBounds(265, 316, 349, 18); 480 | Mixer.Info[] soundDevices = AudioSystem.getMixerInfo(); 481 | for (Mixer.Info device : soundDevices) { 482 | soundDeviceList.addItem(device.getName()); 483 | } 484 | 485 | configurationPanel.add(soundDeviceList); 486 | 487 | // config button bounds 488 | Rectangle configButtonBounds = new Rectangle(0, portBounds.y + portBounds.height + 20, 130, 18); 489 | 490 | // config apply button 491 | JButton applyButton = new JButton(texts.getString("applyButton")); 492 | applyButton.addActionListener((e) -> { 493 | setConfig(); 494 | }); 495 | applyButton.setBounds(new Rectangle(12, 345, 130, 18)); 496 | configurationPanel.add(applyButton); 497 | 498 | configButtonBounds.x += configButtonBounds.width + 10; 499 | configButtonBounds.width = 100; 500 | 501 | // config load button 502 | JButton loadButton = new JButton(texts.getString("loadButton")); 503 | loadButton.addActionListener((event) -> { 504 | JFileChooser fileChooser = new JFileChooser(""); 505 | if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { 506 | File file = fileChooser.getSelectedFile(); 507 | 508 | try { 509 | app.getConfig().load(file.getPath()); 510 | } catch (Exception e) { 511 | log.log(Level.SEVERE, "Invalid configuration file.", e); 512 | showErrorResource("invalidConfigTitle", "invalidConfigText"); 513 | 514 | return; 515 | } 516 | 517 | loadConfig(); 518 | } 519 | }); 520 | 521 | loadButton.setBounds(new Rectangle(153, 345, 100, 18)); 522 | configurationPanel.add(loadButton); 523 | 524 | configButtonBounds.x += configButtonBounds.width + 10; 525 | configButtonBounds.width = 120; 526 | 527 | // config save button 528 | JButton saveButton = new JButton(texts.getString("saveButton")); 529 | saveButton.addActionListener((event) -> { 530 | JFileChooser fileChooser = new JFileChooser(""); 531 | if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { 532 | File file = fileChooser.getSelectedFile(); 533 | 534 | try { 535 | setConfig(); 536 | app.getConfig().save(file.getPath()); 537 | } catch (Exception ex) { 538 | log.log(Level.SEVERE, "Failed to save configuration file.", ex); 539 | showErrorResource("failedConfigTitle", "failedConfigText"); 540 | 541 | return; 542 | } 543 | } 544 | }); 545 | 546 | saveButton.setBounds(new Rectangle(265, 345, 110, 18)); 547 | configurationPanel.add(saveButton); 548 | 549 | JPanel masterPanel = new JPanel(); 550 | masterPanel.setBorder(new TitledBorder(null, texts.getString("masterPanel"), TitledBorder.LEADING, 551 | TitledBorder.TOP, null, null)); 552 | masterPanel.setBounds(174, 22, 183, 92); 553 | configurationPanel.add(masterPanel); 554 | masterPanel.setLayout(null); 555 | 556 | // master name field 557 | masterIP = new JTextField(); 558 | masterIP.setBounds(12, 20, 159, 18); 559 | masterPanel.add(masterIP); 560 | 561 | // master add button 562 | JButton masterAdd = new JButton(texts.getString("masterAdd")); 563 | masterAdd.setBounds(12, 42, 159, 18); 564 | masterPanel.add(masterAdd); 565 | 566 | // master remove button 567 | JButton masterRemove = new JButton(texts.getString("masterRemove")); 568 | masterRemove.setBounds(12, 64, 159, 18); 569 | masterPanel.add(masterRemove); 570 | 571 | // serial invert 572 | invert = new JCheckBox(texts.getString("invert")); 573 | invert.setBounds(174, 268, 141, 18); 574 | configurationPanel.add(invert); 575 | 576 | JPanel pttPanel = new JPanel(); 577 | pttPanel.setBorder(new TitledBorder(null, texts.getString("pttPanel"), TitledBorder.LEADING, TitledBorder.TOP, 578 | null, null)); 579 | pttPanel.setBounds(174, 126, 440, 130); 580 | configurationPanel.add(pttPanel); 581 | pttPanel.setLayout(null); 582 | 583 | serialPanel = new JPanel(); 584 | serialPanel.setBounds(12, 20, 183, 100); 585 | pttPanel.add(serialPanel); 586 | serialPanel.setBorder(new TitledBorder(null, texts.getString("serialPanel"), TitledBorder.LEADING, 587 | TitledBorder.TOP, null, null)); 588 | serialPanel.setLayout(null); 589 | 590 | // serial port 591 | serialPortList = new JComboBox<>(); 592 | serialPortList.setBounds(12, 20, 151, 18); 593 | serialPanel.add(serialPortList); 594 | 595 | // serial pin 596 | serialPin = new JComboBox<>(); 597 | serialPin.setBounds(12, 42, 151, 18); 598 | serialPanel.add(serialPin); 599 | serialPin.addItem("DTR"); // index 0 = SerialPortComm.DTR 600 | serialPin.addItem("RTS"); 601 | 602 | radioUseSerial = new JRadioButton(""); 603 | radioUseSerial.setSelected(true); 604 | radioUseSerial.setBounds(154, 69, 21, 23); 605 | radioUseSerial.setEnabled(false); 606 | serialPanel.add(radioUseSerial); 607 | 608 | gpioPanel = new JPanel(); 609 | gpioPanel.setBounds(201, 20, 227, 100); 610 | pttPanel.add(gpioPanel); 611 | gpioPanel.setBorder(new TitledBorder(null, texts.getString("gpioPanel"), TitledBorder.LEADING, TitledBorder.TOP, 612 | null, null)); 613 | gpioPanel.setLayout(null); 614 | gpioPanel.setEnabled(false); 615 | 616 | raspiList = new JComboBox<>(); 617 | raspiList.setBounds(12, 20, 203, 18); 618 | gpioPanel.add(raspiList); 619 | raspiList.addItem(texts.getString("itemDeactivated")); 620 | raspiList.setEnabled(false); 621 | 622 | gpioList = new JComboBox<>(); 623 | gpioList.setBounds(12, 42, 203, 18); 624 | gpioPanel.add(gpioList); 625 | gpioList.addItem(texts.getString("itemDeactivated")); 626 | gpioList.setEnabled(false); 627 | 628 | btnGpioPins = new JButton(texts.getString("btnGpioPins")); 629 | btnGpioPins.setBounds(12, 70, 115, 18); 630 | gpioPanel.add(btnGpioPins); 631 | btnGpioPins.setEnabled(false); 632 | 633 | radioUseGpio = new JRadioButton(""); 634 | radioUseGpio.setBounds(194, 69, 21, 23); 635 | gpioPanel.add(radioUseGpio); 636 | radioUseGpio.setEnabled(true); 637 | 638 | radioUseGpio.addActionListener((e) -> { 639 | if (radioUseGpio.isSelected()) { 640 | radioUseGpio.setEnabled(false); 641 | radioUseSerial.setEnabled(true); 642 | radioUseSerial.setSelected(false); 643 | gpioPanel.setEnabled(true); 644 | serialPanel.setEnabled(false); 645 | raspiList.setEnabled(true); 646 | gpioList.setEnabled(true); 647 | btnGpioPins.setEnabled(true); 648 | 649 | serialPortList.setEnabled(false); 650 | serialPin.setEnabled(false); 651 | } 652 | }); 653 | btnGpioPins.addActionListener((e) -> { 654 | EventQueue.invokeLater(() -> { 655 | try { 656 | new GpioView().setVisible(true); 657 | } catch (Exception ex) { 658 | log.log(Level.SEVERE, "Failed to open GPIO view.", ex); 659 | } 660 | }); 661 | }); 662 | 663 | raspiList.addActionListener((e) -> { 664 | gpioList.removeAllItems(); 665 | gpioList.addItem(texts.getString("itemDeactivated")); 666 | 667 | BoardType type = BoardType.valueOf(raspiList.getSelectedItem().toString()); 668 | for (Pin p : RaspiPin.allPins(type)) { 669 | gpioList.addItem(p.toString()); 670 | } 671 | }); 672 | 673 | radioUseSerial.addActionListener((e) -> { 674 | if (radioUseSerial.isSelected()) { 675 | radioUseSerial.setEnabled(false); 676 | radioUseGpio.setEnabled(true); 677 | radioUseGpio.setSelected(false); 678 | serialPanel.setEnabled(true); 679 | gpioPanel.setEnabled(false); 680 | serialPortList.setEnabled(true); 681 | serialPin.setEnabled(true); 682 | 683 | raspiList.setEnabled(false); 684 | gpioList.setEnabled(false); 685 | btnGpioPins.setEnabled(false); 686 | } 687 | }); 688 | 689 | java.util.List serialPorts = SerialPortComm.getPorts(); 690 | for (int i = 0; i < serialPorts.size(); i++) { 691 | serialPortList.addItem(serialPorts.get(i)); 692 | } 693 | 694 | for (BoardType bt : BoardType.values()) { 695 | raspiList.addItem(bt.toString()); 696 | } 697 | 698 | masterRemove.addActionListener((e) -> { 699 | if (masterList.getSelectedItem() != null && showConfirmResource("delMasterTitle", "delMasterText")) { 700 | masterList.remove(masterList.getSelectedIndex()); 701 | } 702 | }); 703 | 704 | masterAdd.addActionListener((e) -> { 705 | String master = masterIP.getText(); 706 | if (master.isEmpty()) { 707 | return; 708 | } 709 | 710 | // check if master is already in list 711 | for (String m : masterList.getItems()) { 712 | if (m.equalsIgnoreCase(master)) { 713 | showErrorResource("addMasterFailTitle", "addMasterFailText"); 714 | return; 715 | } 716 | } 717 | 718 | masterList.add(master); 719 | masterIP.setText(""); 720 | }); 721 | 722 | // show window 723 | pack(); 724 | setVisible(true); 725 | 726 | loadConfig(); 727 | 728 | // create tray icon if requested 729 | if (withTrayIcon) { 730 | Image trayImage = Toolkit.getDefaultToolkit().getImage("icon.ico"); 731 | 732 | PopupMenu trayMenu = new PopupMenu(texts.getString("trayMenu")); 733 | MenuItem menuItem = new MenuItem(texts.getString("trayMenuShow")); 734 | menuItem.addActionListener((e) -> { 735 | setExtendedState(Frame.NORMAL); 736 | setVisible(true); 737 | }); 738 | trayMenu.add(menuItem); 739 | 740 | trayIcon = new TrayIcon(trayImage, "RasPager", trayMenu); 741 | try { 742 | SystemTray.getSystemTray().add(trayIcon); 743 | } catch (AWTException e) { 744 | log.warning("Failed to add tray icon."); 745 | } 746 | } 747 | } 748 | 749 | // set connection status 750 | public void setStatus(boolean connected) { 751 | if (connected) { 752 | statusDisplay.setText(texts.getString("statusDisplayCon")); 753 | } else { 754 | statusDisplay.setText(texts.getString("statusDisplayDis")); 755 | } 756 | } 757 | 758 | // run search 759 | public void runSearch(boolean run) { 760 | if (searchAddress.getText().isEmpty()) { 761 | showErrorResource("searchErrorTitle", "noSearchAddress"); 762 | return; 763 | } 764 | 765 | if (run) { 766 | if (app.isServerRunning()) { 767 | if (!showConfirmResource("searchRunningTitle", "searchRunningText")) { 768 | return; 769 | } 770 | 771 | app.stopServer(false); 772 | } 773 | 774 | app.startScheduler(true); 775 | 776 | searchStart.setEnabled(false); 777 | searchStop.setEnabled(true); 778 | 779 | searchStepWidth.setEditable(false); 780 | searchAddress.setEditable(false); 781 | 782 | startButton.setEnabled(false); 783 | } else { 784 | app.stopScheduler(); 785 | 786 | searchStart.setEnabled(true); 787 | searchStop.setEnabled(false); 788 | 789 | searchStepWidth.setEditable(true); 790 | searchAddress.setEditable(true); 791 | 792 | startButton.setEnabled(true); 793 | } 794 | } 795 | 796 | public void setConfig() { 797 | Configuration config = app.getConfig(); 798 | 799 | config.setInt(ConfigKeys.NET_PORT, Integer.parseInt(port.getText())); 800 | config.setString(ConfigKeys.NET_MASTERS, String.join(" ", masterList.getItems())); 801 | 802 | if (!radioUseSerial.isSelected()) { 803 | config.setBoolean(ConfigKeys.SERIAL_USE, false); 804 | } else { 805 | config.setBoolean(ConfigKeys.SERIAL_USE, true); 806 | config.setString(ConfigKeys.SERIAL_PORT, serialPortList.getSelectedItem().toString()); 807 | config.setString(ConfigKeys.SERIAL_PIN, serialPin.getSelectedItem().toString()); 808 | } 809 | 810 | if (!radioUseGpio.isSelected()) { 811 | config.setBoolean(ConfigKeys.GPIO_USE, false); 812 | } else { 813 | config.setBoolean(ConfigKeys.GPIO_USE, true); 814 | config.setString(ConfigKeys.GPIO_PIN, gpioList.getSelectedItem().toString()); 815 | config.setString(ConfigKeys.GPIO_RASPI_REV, raspiList.getSelectedItem().toString()); 816 | } 817 | 818 | config.setBoolean(ConfigKeys.INVERT, invert.isSelected()); 819 | config.setInt(ConfigKeys.TX_DELAY, Integer.parseInt(delay.getText())); 820 | 821 | if (soundDeviceList.getSelectedItem() != null) { 822 | config.setString(ConfigKeys.SDR_DEVICE, soundDeviceList.getSelectedItem().toString()); 823 | } 824 | 825 | config.setFloat(ConfigKeys.SDR_CORRECTION, correctionSlider.getValue() / 100.0f); 826 | 827 | if (app.isRunning()) { 828 | if (showConfirmResource("cfgRunningTitle", "cfgRunningText")) { 829 | app.stopServer(false); 830 | app.startServer(false); 831 | 832 | startButton.setText("Server stoppen"); 833 | } 834 | } 835 | } 836 | 837 | public void loadConfig() { 838 | Configuration config = app.getConfig(); 839 | 840 | port.setText(Integer.toString(config.getInt(ConfigKeys.NET_PORT, 1337))); 841 | 842 | // load masters 843 | masterList.removeAll(); 844 | String value = config.getString(ConfigKeys.NET_MASTERS, null); 845 | if (value != null && !value.isEmpty()) { 846 | Arrays.stream(value.split(" +")).forEach((m) -> masterList.add(m)); 847 | } 848 | 849 | // load serial 850 | serialPortList.setSelectedItem(config.getString(ConfigKeys.SERIAL_PORT, null)); 851 | serialPin.setSelectedItem(config.getString(ConfigKeys.SERIAL_PIN, null)); 852 | delay.setText(Integer.toString(config.getInt(ConfigKeys.TX_DELAY, 0))); 853 | 854 | // load raspi / gpio 855 | gpioList.removeAllItems(); 856 | gpioList.addItem(texts.getString("itemDeactivated")); 857 | 858 | invert.setSelected(config.getBoolean(ConfigKeys.INVERT, false)); 859 | 860 | if (config.getBoolean(ConfigKeys.SERIAL_USE, false)) { 861 | radioUseSerial.setSelected(true); 862 | radioUseSerial.setEnabled(false); 863 | radioUseGpio.setEnabled(true); 864 | radioUseGpio.setSelected(false); 865 | serialPanel.setEnabled(true); 866 | gpioPanel.setEnabled(false); 867 | serialPortList.setEnabled(true); 868 | serialPin.setEnabled(true); 869 | 870 | raspiList.setEnabled(false); 871 | gpioList.setEnabled(false); 872 | btnGpioPins.setEnabled(false); 873 | } else { 874 | radioUseGpio.setSelected(true); 875 | radioUseGpio.setEnabled(false); 876 | radioUseSerial.setEnabled(true); 877 | radioUseSerial.setSelected(false); 878 | gpioPanel.setEnabled(true); 879 | serialPanel.setEnabled(false); 880 | raspiList.setEnabled(true); 881 | gpioList.setEnabled(true); 882 | btnGpioPins.setEnabled(true); 883 | 884 | serialPortList.setEnabled(false); 885 | serialPin.setEnabled(false); 886 | } 887 | 888 | value = config.getString(ConfigKeys.GPIO_RASPI_REV, null); 889 | if (value != null) { 890 | BoardType type = BoardType.valueOf(value); 891 | raspiList.setSelectedItem(type.toString()); 892 | 893 | for (Pin p : RaspiPin.allPins(type)) { 894 | gpioList.addItem(p.getName()); 895 | } 896 | 897 | gpioList.setSelectedItem(config.getString(ConfigKeys.GPIO_PIN, null)); 898 | } 899 | 900 | soundDeviceList.setSelectedItem(config.getString(ConfigKeys.SDR_DEVICE, null)); 901 | // Correction is loaded by transmitter 902 | updateCorrection(); 903 | } 904 | 905 | public boolean useSerial() { 906 | return this.radioUseSerial.isSelected(); 907 | } 908 | 909 | public boolean useGpio() { 910 | return this.radioUseGpio.isSelected(); 911 | } 912 | 913 | // reset buttons 914 | public void resetButtons() { 915 | startButton.setText(texts.getString("startButtonStart")); 916 | 917 | searchStart.setEnabled(true); 918 | searchStop.setEnabled(false); 919 | 920 | searchStepWidth.setEditable(true); 921 | searchAddress.setEditable(true); 922 | 923 | setStatus(false); 924 | } 925 | 926 | public void showError(String title, String message) { 927 | JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); 928 | } 929 | 930 | private void showErrorResource(String title, String text) { 931 | JOptionPane.showMessageDialog(null, texts.getString(text), texts.getString(title), JOptionPane.ERROR_MESSAGE); 932 | } 933 | 934 | private boolean showConfirmResource(String title, String message) { 935 | return JOptionPane.showConfirmDialog(this, texts.getString(message), texts.getString(title), 936 | JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION; 937 | } 938 | 939 | public void updateTimeSlots(TimeSlots slots) { 940 | this.timeSlots = slots; 941 | slotDisplay.repaint(); 942 | } 943 | 944 | public String getStepWidth() { 945 | if (searchStepWidth.getText().isEmpty()) { 946 | searchStepWidth.setText(Float.toString(app.getSearchStepSize())); 947 | } 948 | 949 | return searchStepWidth.getText(); 950 | } 951 | 952 | public String getSkyperAddress() { 953 | if (!searchAddress.getText().isEmpty()) { 954 | int intAddr = Integer.parseInt(searchAddress.getText()); 955 | return Integer.toHexString(intAddr); 956 | } else { 957 | return null; 958 | } 959 | } 960 | 961 | private void updateCorrection() { 962 | float correction = app.getConfig().getFloat(ConfigKeys.SDR_CORRECTION, 0.0f); 963 | correctionActual.setText(String.format("%+4.2f", correction)); 964 | correctionSlider.setValue((int) (correction * 100)); 965 | } 966 | 967 | public void updateCorrection(float correction) { 968 | correctionActual.setText(String.format("%+4.2f", correction)); 969 | correctionSlider.setValue((int) (correction * 100)); 970 | } 971 | } -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/Scheduler.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Deque; 5 | import java.util.List; 6 | import java.util.TimerTask; 7 | import java.util.concurrent.atomic.AtomicBoolean; 8 | import java.util.function.Consumer; 9 | import java.util.logging.Level; 10 | import java.util.logging.Logger; 11 | 12 | class Scheduler extends TimerTask { 13 | protected enum State { 14 | AWAITING_SLOT, DATA_ENCODED, SLOT_STILL_ALLOWED 15 | } 16 | 17 | private static final Logger log = Logger.getLogger(Scheduler.class.getName()); 18 | // max time value (2^16) 19 | protected static final int MAX = 65536; 20 | protected static final int MAX_ENCODE_TIME_100MS = 3; 21 | protected static final int TIMERCYCLE_MS = 10; 22 | 23 | protected AtomicBoolean canceled = new AtomicBoolean(false); 24 | protected final TimeSlots slots = new TimeSlots(); 25 | protected final Deque messageQueue; 26 | protected final Transmitter transmitter; 27 | private final int txDelay; 28 | 29 | protected int time = 0; 30 | protected int delay = 0; 31 | protected Consumer updateTimeSlotsHandler; 32 | protected State schedulerState = State.AWAITING_SLOT; 33 | protected List codeWords; 34 | protected byte[] rawData; 35 | 36 | public Scheduler(Configuration config, Deque messageQueue, Transmitter transmitter) { 37 | this.messageQueue = messageQueue; 38 | this.transmitter = transmitter; 39 | 40 | this.txDelay = config.getInt(ConfigKeys.TX_DELAY); 41 | } 42 | 43 | public void setUpdateTimeSlotsHandler(Consumer handler) { 44 | updateTimeSlotsHandler = handler; 45 | } 46 | 47 | @Override 48 | public boolean cancel() { 49 | canceled.set(true); 50 | 51 | return super.cancel(); 52 | } 53 | 54 | @Override 55 | public void run() { 56 | if (canceled.get()) { 57 | return; 58 | } 59 | 60 | time = ((int) (System.currentTimeMillis() / 100) + delay) % MAX; 61 | 62 | if (slots.hasChanged(time) && updateTimeSlotsHandler != null) { 63 | // log.fine("Updating time slots."); 64 | updateTimeSlotsHandler.accept(slots); 65 | } 66 | 67 | switch (schedulerState) { 68 | case AWAITING_SLOT: 69 | encodeData(); 70 | break; 71 | case DATA_ENCODED: 72 | sendData(); 73 | break; 74 | case SLOT_STILL_ALLOWED: 75 | stillAllowed(); 76 | break; 77 | default: 78 | log.log(Level.WARNING, "Unknown state {0}.", schedulerState); 79 | } 80 | } 81 | 82 | private void encodeData() { 83 | try { 84 | if (slots.isNextAllowed(time) && !messageQueue.isEmpty() 85 | && TimeSlots.getTimeToNextSlot(time) <= MAX_ENCODE_TIME_100MS) { 86 | int nextAllowed = TimeSlots.getNextIndex(time); 87 | int allowedCount = slots.getCount(nextAllowed); 88 | 89 | if (updateData(allowedCount)) { 90 | rawData = transmitter.encode(codeWords); 91 | schedulerState = State.DATA_ENCODED; 92 | log.log(Level.FINE, "state = {0}", schedulerState); 93 | } 94 | } 95 | } catch (Throwable t) { 96 | log.log(Level.SEVERE, "Failed to encode data.", t); 97 | } 98 | } 99 | 100 | private void sendData() { 101 | if (slots.get(TimeSlots.getIndex(time))) { 102 | log.fine("Activating transmitter."); 103 | try { 104 | transmitter.send(rawData); 105 | log.fine("Data sent"); 106 | } catch (Throwable t) { 107 | log.log(Level.SEVERE, "Failed to send data.", t); 108 | } finally { 109 | schedulerState = State.SLOT_STILL_ALLOWED; 110 | } 111 | 112 | log.log(Level.FINE, "state = {0}", schedulerState); 113 | } 114 | } 115 | 116 | private void stillAllowed() { 117 | try { 118 | if (slots.isAllowed(time) && !messageQueue.isEmpty()) { 119 | int currentSlot = TimeSlots.getIndex(time); 120 | int count = slots.getCount(currentSlot); 121 | 122 | if (updateData(count)) { 123 | rawData = transmitter.encode(codeWords); 124 | schedulerState = State.DATA_ENCODED; 125 | } 126 | } else { 127 | schedulerState = State.AWAITING_SLOT; 128 | } 129 | } catch (Throwable t) { 130 | log.log(Level.SEVERE, "Failed to encode data.", t); 131 | schedulerState = State.AWAITING_SLOT; 132 | } 133 | 134 | log.log(Level.FINE, "state = {0}", schedulerState); 135 | } 136 | 137 | /** 138 | * Gets data depending on the given slot count. 139 | * 140 | * @param slotCount 141 | * Slot count. 142 | * @return Code words to send. 143 | */ 144 | private boolean updateData(int slotCount) { 145 | // send batches 146 | // max batches per slot: (slot time - praeambel time) / bps / ((frames + 147 | // (1 = sync)) * bits per frame) 148 | // (3,75 - 0,48) * 1200 / ((16 + 1) * 32) 149 | int maxBatch = (int) ((6.40 * slotCount - 0.48 - txDelay / 1000) * 1200 / 544); 150 | int msgCount = 0; 151 | 152 | codeWords = new ArrayList<>(); 153 | 154 | // add praeembel 155 | for (int i = 0; i < 18; i++) { 156 | codeWords.add(Pocsag.PRAEAMBLE); 157 | } 158 | 159 | while (!messageQueue.isEmpty()) { 160 | Message message = messageQueue.pop(); 161 | 162 | // get codewords and frame position 163 | List cwBuf = message.getCodeWords(); 164 | int framePos = cwBuf.get(0); 165 | int cwCount = cwBuf.size() - 1; 166 | 167 | // (data.size() - 18) / 17 = aktBatches 168 | // aktBatches + (cwCount + 2 * framePos) / 16 + 1 = Batches NACH 169 | // hinzufügen 170 | // also Batches NACH hinzufügen > maxBatches, dann keine neue 171 | // Nachricht holen 172 | // if count of batches + this message is greater than max batches 173 | if (((codeWords.size() - 18) / 17 + (cwCount + 2 * framePos) / 16 + 1) > maxBatch) { 174 | messageQueue.addFirst(message); 175 | break; 176 | } 177 | 178 | ++msgCount; 179 | 180 | // each batch starts with a sync code word 181 | codeWords.add(Pocsag.SYNC); 182 | 183 | // add idle code words until frame position is reached 184 | for (int c = 0; c < framePos; c++) { 185 | codeWords.add(Pocsag.IDLE); 186 | codeWords.add(Pocsag.IDLE); 187 | } 188 | 189 | // add actual payload 190 | for (int c = 1; c < cwBuf.size(); c++) { 191 | if ((codeWords.size() - 18) % 17 == 0) { 192 | codeWords.add(Pocsag.SYNC); 193 | } 194 | 195 | codeWords.add(cwBuf.get(c)); 196 | } 197 | 198 | // fill batch with idle-words 199 | while ((codeWords.size() - 18) % 17 != 0) { 200 | codeWords.add(Pocsag.IDLE); 201 | } 202 | } 203 | 204 | if (msgCount > 0) { 205 | log.fine(String.format("Batches used: %1$d / %2$d", ((codeWords.size() - 18) / 17), maxBatch)); 206 | return true; 207 | } else { 208 | return false; 209 | } 210 | } 211 | 212 | public TimeSlots getSlots() { 213 | return slots; 214 | } 215 | 216 | public void setTimeSlots(String s) { 217 | slots.setSlots(s); 218 | } 219 | 220 | /** 221 | * Gets current time. 222 | * 223 | * @return Current time. 224 | */ 225 | public int getTime() { 226 | return time; 227 | } 228 | 229 | /** 230 | * Sets time correction. 231 | * 232 | * @param delay 233 | * Time correction. 234 | */ 235 | public void correctTime(int delay) { 236 | this.delay += delay; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/SearchScheduler.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Deque; 5 | import java.util.List; 6 | import java.util.logging.Level; 7 | import java.util.logging.Logger; 8 | 9 | import de.rwth_aachen.afu.raspager.sdr.SDRTransmitter; 10 | 11 | class SearchScheduler extends Scheduler { 12 | private static final Logger log = Logger.getLogger(SearchScheduler.class.getName()); 13 | private final RasPagerService service; 14 | 15 | public SearchScheduler(RasPagerService service, Deque messageQueue) { 16 | super(service.getConfig(), messageQueue, service.getTransmitter()); 17 | this.service = service; 18 | } 19 | 20 | @Override 21 | public void run() { 22 | try { 23 | if (updateData()) { 24 | log.fine("Encoding data."); 25 | rawData = transmitter.encode(codeWords); 26 | log.fine("Sending data."); 27 | transmitter.send(rawData); 28 | } 29 | } catch (IllegalStateException ex) { 30 | // This happens when the task is cancelled while executing. 31 | if (!canceled.get()) { 32 | log.log(Level.SEVERE, "SearchScheduler interrupted.", ex); 33 | } 34 | } catch (Throwable t) { 35 | log.log(Level.SEVERE, "SearchScheduler failed.", t); 36 | } 37 | } 38 | 39 | private boolean updateData() { 40 | if (service.getWindow() == null) { 41 | throw new IllegalStateException("Main window is null."); 42 | } 43 | 44 | SDRTransmitter sdr = (SDRTransmitter) transmitter; 45 | 46 | float correction = sdr.getCorrection(); 47 | float stepSize = service.getStepSize(); 48 | 49 | if (correction < 1.0f) { 50 | correction += stepSize; 51 | if (correction > 1.0f) { 52 | correction = 1.0f; 53 | } 54 | 55 | sdr.setCorrection(correction); 56 | // TODO Refactor 57 | service.getWindow().updateCorrection(correction); 58 | } else { 59 | service.stopSearching(); 60 | } 61 | 62 | codeWords = new ArrayList<>(); 63 | for (int i = 0; i < 18; ++i) { 64 | codeWords.add(Pocsag.PRAEAMBLE); 65 | } 66 | 67 | addMessage(new Message(("#00 5:1:9C8:0:000000 010112").split(":"))); 68 | 69 | // TODO Remove? Empty field is checked in button handler. 70 | String addr = service.getWindow().getSkyperAddress(); 71 | if (addr != null && !addr.isEmpty()) { 72 | String[] parts = new String[] { "#00 6", "1", addr, "3", 73 | String.format("correction=%+4.2f", sdr.getCorrection()) }; 74 | addMessage(new Message(parts)); 75 | 76 | return true; 77 | } else { 78 | codeWords = null; 79 | 80 | return false; 81 | } 82 | } 83 | 84 | private void addMessage(Message message) { 85 | // add sync-word (start of batch) 86 | codeWords.add(Pocsag.SYNC); 87 | 88 | // get codewords of message 89 | List cwBuf = message.getCodeWords(); 90 | int framePos = cwBuf.get(0); 91 | 92 | // add idle-words until frame position is reached 93 | for (int c = 0; c < framePos; c++) { 94 | codeWords.add(Pocsag.IDLE); 95 | codeWords.add(Pocsag.IDLE); 96 | } 97 | 98 | // add codewords of message 99 | for (int c = 1; c < cwBuf.size(); c++) { 100 | if ((codeWords.size() - 18) % 17 == 0) 101 | codeWords.add(Pocsag.SYNC); 102 | codeWords.add(cwBuf.get(c)); 103 | } 104 | 105 | // fill last batch with idle-words 106 | while ((codeWords.size() - 18) % 17 != 0) { 107 | codeWords.add(Pocsag.IDLE); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/Server.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.IntConsumer; 5 | import java.util.function.IntSupplier; 6 | import java.util.logging.Level; 7 | import java.util.logging.Logger; 8 | 9 | import io.netty.bootstrap.ServerBootstrap; 10 | import io.netty.channel.ChannelFuture; 11 | import io.netty.channel.ChannelInitializer; 12 | import io.netty.channel.ChannelPipeline; 13 | import io.netty.channel.EventLoopGroup; 14 | import io.netty.channel.nio.NioEventLoopGroup; 15 | import io.netty.channel.socket.SocketChannel; 16 | import io.netty.channel.socket.nio.NioServerSocketChannel; 17 | import io.netty.handler.codec.DelimiterBasedFrameDecoder; 18 | import io.netty.handler.codec.Delimiters; 19 | import io.netty.handler.codec.string.StringDecoder; 20 | import io.netty.handler.codec.string.StringEncoder; 21 | 22 | /** 23 | * RasPager server implementation. 24 | * 25 | * @author Philipp Thiel 26 | */ 27 | final class Server implements Runnable { 28 | private static final Logger log = Logger.getLogger(Server.class.getName()); 29 | private static final StringEncoder encoder = new StringEncoder(); 30 | private static final StringDecoder decoder = new StringDecoder(); 31 | private final ServerHandler protocol = new ServerHandler(); 32 | private final MasterServerFilter ipFilter; 33 | private final int port; 34 | private ChannelFuture serverFuture; 35 | 36 | /** 37 | * Creates a new server instance. 38 | * 39 | * @param port 40 | * Port number to listen on. 41 | * @param masters 42 | * Master server list (null to accept all incoming connections). 43 | */ 44 | public Server(int port, String[] masters) { 45 | this.port = port; 46 | 47 | if (masters != null) { 48 | ipFilter = new MasterServerFilter(masters); 49 | } else { 50 | ipFilter = null; 51 | } 52 | } 53 | 54 | /** 55 | * Sets the new message handler. 56 | * 57 | * @param messageHandler 58 | * Handler to use. 59 | */ 60 | public void setAddMessageHandler(Consumer messageHandler) { 61 | protocol.setAddMessageHandler(messageHandler); 62 | } 63 | 64 | /** 65 | * Sets the time correction handler. 66 | * 67 | * @param timeCorrectionHandler 68 | * Handler to use. 69 | */ 70 | public void setTimeCorrectionHandler(IntConsumer timeCorrectionHandler) { 71 | protocol.setTimeCorrectionHandler(timeCorrectionHandler); 72 | } 73 | 74 | /** 75 | * Sets the time slots handler. 76 | * 77 | * @param timeSlotsHandler 78 | * Handler to use. 79 | */ 80 | public void setTimeSlotsHandler(Consumer timeSlotsHandler) { 81 | protocol.setTimeSlotsHandler(timeSlotsHandler); 82 | } 83 | 84 | /** 85 | * Sets the get time handler. 86 | * 87 | * @param timeHandler 88 | * Handler to use. 89 | */ 90 | public void setGetTimeHandler(IntSupplier timeHandler) { 91 | protocol.setTimeHandler(timeHandler); 92 | } 93 | 94 | /** 95 | * Sets the handler for new connections. 96 | * 97 | * @param handler 98 | * Handler to use. 99 | */ 100 | public void setConnectionHandler(Runnable handler) { 101 | protocol.setConnectHandler(handler); 102 | } 103 | 104 | /** 105 | * Sets the handler for closed connections. 106 | * 107 | * @param handler 108 | * Handler to use. 109 | */ 110 | public void setDisconnectHandler(Runnable handler) { 111 | protocol.setDisconnectHandler(handler); 112 | } 113 | 114 | @Override 115 | public void run() { 116 | EventLoopGroup bossGroup = new NioEventLoopGroup(1); 117 | EventLoopGroup workerGroup = new NioEventLoopGroup(); 118 | 119 | try { 120 | ServerBootstrap b = new ServerBootstrap(); 121 | b.group(bossGroup, workerGroup); 122 | b.channel(NioServerSocketChannel.class); 123 | 124 | // Define channel initializer 125 | b.childHandler(new ChannelInitializer() { 126 | @Override 127 | protected void initChannel(SocketChannel ch) throws Exception { 128 | ChannelPipeline pip = ch.pipeline(); 129 | 130 | if (ipFilter != null) { 131 | pip.addLast("filter", ipFilter); 132 | } 133 | 134 | pip.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter())); 135 | // Static as both encoder and decoder are sharable. 136 | pip.addLast("decoder", decoder); 137 | pip.addLast("encoder", encoder); 138 | // Our custom message handler 139 | pip.addLast("protocol", protocol); 140 | } 141 | }); 142 | 143 | serverFuture = b.bind(port).sync(); 144 | // Wait for shutdown 145 | serverFuture.channel().closeFuture().sync(); 146 | } catch (InterruptedException e) { 147 | log.log(Level.SEVERE, "Funkruf server interrupted.", e); 148 | } catch (Throwable t) { 149 | log.log(Level.SEVERE, "Exception in server.", t); 150 | } finally { 151 | bossGroup.shutdownGracefully(); 152 | workerGroup.shutdownGracefully(); 153 | } 154 | } 155 | 156 | /** 157 | * Stops the server if it is running. This method will block until the 158 | * server is stopped. 159 | */ 160 | public void shutdown() { 161 | try { 162 | if (serverFuture != null) { 163 | serverFuture.channel().close().sync(); 164 | } 165 | } catch (InterruptedException e) { 166 | log.log(Level.WARNING, "Close interrupted.", e); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/ServerHandler.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | import java.util.function.Consumer; 5 | import java.util.function.IntConsumer; 6 | import java.util.function.IntSupplier; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | import io.netty.channel.ChannelHandler.Sharable; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.SimpleChannelInboundHandler; 13 | 14 | /** 15 | * This class handles incoming packets like new messages to send from a client 16 | * connection. 17 | */ 18 | @Sharable 19 | final class ServerHandler extends SimpleChannelInboundHandler { 20 | private static final Logger log = Logger.getLogger(ServerHandler.class.getName()); 21 | private final AtomicInteger connectionCount = new AtomicInteger(0); 22 | private Consumer messageHandler; 23 | private IntConsumer timeCorrectionHandler; 24 | private Consumer timeSlotsHandler; 25 | private IntSupplier timeHandler; 26 | private Runnable connectHandler; 27 | private Runnable disconnectHandler; 28 | 29 | /** 30 | * Sets the handler for new message packets. 31 | * 32 | * @param messageHandler 33 | * Handler to use. 34 | */ 35 | public void setAddMessageHandler(Consumer messageHandler) { 36 | this.messageHandler = messageHandler; 37 | } 38 | 39 | /** 40 | * Sets the handler for time correction packets. 41 | * 42 | * @param timeCorrectionHandler 43 | * Handler to use. 44 | */ 45 | public void setTimeCorrectionHandler(IntConsumer timeCorrectionHandler) { 46 | this.timeCorrectionHandler = timeCorrectionHandler; 47 | } 48 | 49 | /** 50 | * Sets the handler for time slot activation packets. 51 | * 52 | * @param timeSlotsHandler 53 | * Handler to use. 54 | */ 55 | public void setTimeSlotsHandler(Consumer timeSlotsHandler) { 56 | this.timeSlotsHandler = timeSlotsHandler; 57 | } 58 | 59 | /** 60 | * Sets the handler for time packets. 61 | * 62 | * @param timeHandler 63 | * Handler to use. 64 | */ 65 | public void setTimeHandler(IntSupplier timeHandler) { 66 | this.timeHandler = timeHandler; 67 | } 68 | 69 | /** 70 | * Sets the connect handler which is called when a new connection is 71 | * accepted. 72 | * 73 | * @param handler 74 | * Handler to use. 75 | */ 76 | public void setConnectHandler(Runnable handler) { 77 | connectHandler = handler; 78 | } 79 | 80 | /** 81 | * Sets the disconnect handler which is called when a connection is closed. 82 | * 83 | * @param handler 84 | * Handler to use. 85 | */ 86 | public void setDisconnectHandler(Runnable handler) { 87 | disconnectHandler = handler; 88 | } 89 | 90 | @Override 91 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 92 | log.fine("Accepted new connection."); 93 | 94 | // TODO Adjust version string 95 | ctx.write("[SDRPager v2.0-SCP-#2345678]\r\n"); 96 | ctx.flush(); 97 | 98 | int count = connectionCount.incrementAndGet(); 99 | if (count == 1 && connectHandler != null) { 100 | connectHandler.run(); 101 | } 102 | } 103 | 104 | @Override 105 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 106 | log.fine("Connection closed."); 107 | 108 | int count = connectionCount.decrementAndGet(); 109 | if (count == 0 && disconnectHandler != null) { 110 | disconnectHandler.run(); 111 | } 112 | } 113 | 114 | @Override 115 | public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 116 | ctx.flush(); 117 | } 118 | 119 | @Override 120 | protected void channelRead0(ChannelHandlerContext ctx, String request) throws Exception { 121 | if (request.isEmpty()) { 122 | log.warning("Received empty request."); 123 | return; 124 | } 125 | 126 | char type = request.charAt(0); 127 | log.log(Level.FINE, "Received message of type: {0}", type); 128 | 129 | switch (type) { 130 | case '#': 131 | handleMessage(ctx, request); 132 | break; 133 | case '2': 134 | handleMasterIdentify(ctx, request); 135 | break; 136 | case '3': 137 | handleTimeCorrection(ctx, request); 138 | break; 139 | case '4': 140 | handleTimeSlots(ctx, request); 141 | break; 142 | default: 143 | log.log(Level.WARNING, "Invalid message type: {0}", type); 144 | ackError(ctx); 145 | } 146 | } 147 | 148 | @Override 149 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 150 | log.log(Level.SEVERE, "Exception caught in channel handler.", cause); 151 | ctx.close(); 152 | } 153 | 154 | /** 155 | * Adds a message to the message queue. 156 | * 157 | * @param ctx 158 | * Client connection. 159 | * @param request 160 | * Request which contains the message. 161 | */ 162 | private void handleMessage(ChannelHandlerContext ctx, String request) { 163 | try { 164 | if (messageHandler != null) { 165 | messageHandler.accept(new Message(request)); 166 | 167 | // Send message ID as response 168 | int messageId = Integer.parseInt(request.substring(1, 3), 16); 169 | messageId = (messageId + 1) % 256; 170 | String response = String.format("#%02x +\r\n", messageId); 171 | ctx.write(response); 172 | } else { 173 | log.severe("No message handler registered."); 174 | ackError(ctx); 175 | } 176 | } catch (Throwable t) { 177 | log.log(Level.WARNING, "Failed to add message or send response.", t); 178 | ackError(ctx); 179 | } 180 | } 181 | 182 | /** 183 | * Handles the master identify message. 184 | * 185 | * @param ctx 186 | * Client connection. 187 | * @param request 188 | * Request 189 | */ 190 | private void handleMasterIdentify(ChannelHandlerContext ctx, String request) { 191 | try { 192 | if (timeHandler != null) { 193 | int time = timeHandler.getAsInt(); 194 | String[] parts = request.split(":", 2); 195 | String response = String.format("2:%s:%04x\r\n", parts[1], time); 196 | ctx.write(response); 197 | ackSuccess(ctx); 198 | } else { 199 | log.severe("No time handler registered."); 200 | ackError(ctx); 201 | } 202 | } catch (Throwable t) { 203 | log.log(Level.WARNING, "Failed to handle master packet.", t); 204 | ackError(ctx); 205 | } 206 | } 207 | 208 | /** 209 | * Handles the correct time message. 210 | * 211 | * @param ctx 212 | * Client connection 213 | * @param request 214 | * Time data 215 | */ 216 | private void handleTimeCorrection(ChannelHandlerContext ctx, String request) { 217 | try { 218 | if (timeCorrectionHandler != null) { 219 | String[] parts = request.split(":", 2); 220 | int delay = 0; 221 | if (parts[1].charAt(1) == '+') { 222 | delay = Integer.parseInt(parts[1].substring(1), 16); 223 | } else { 224 | // No need to strip leading "-" char 225 | delay = Integer.parseInt(parts[1], 16); 226 | } 227 | 228 | timeCorrectionHandler.accept(delay); 229 | 230 | ackSuccess(ctx); 231 | } else { 232 | log.severe("No set time correction handler registered."); 233 | ackError(ctx); 234 | } 235 | } catch (Throwable t) { 236 | log.log(Level.WARNING, "Failed to correct time.", t); 237 | ackError(ctx); 238 | } 239 | } 240 | 241 | /** 242 | * Handles the set time slots message. 243 | * 244 | * @param ctx 245 | * Client connection. 246 | * @param request 247 | * Time slot data. 248 | */ 249 | private void handleTimeSlots(ChannelHandlerContext ctx, String request) { 250 | log.fine("TimeSlots"); 251 | try { 252 | if (timeSlotsHandler != null) { 253 | String[] parts = request.split(":", 2); 254 | timeSlotsHandler.accept(parts[1]); 255 | ackSuccess(ctx); 256 | } else { 257 | log.severe("No set time slots handler registered."); 258 | ackError(ctx); 259 | } 260 | } catch (Throwable t) { 261 | log.log(Level.WARNING, "Failed to set time slots.", t); 262 | ackError(ctx); 263 | } 264 | } 265 | 266 | /** 267 | * Sends a success ack to the client. 268 | * 269 | * @param ctx 270 | * Client connection. 271 | */ 272 | private void ackSuccess(ChannelHandlerContext ctx) { 273 | ctx.write("+\r\n"); 274 | } 275 | 276 | /** 277 | * Sends an error ack to the client. 278 | * 279 | * @param ctx 280 | * Client connection. 281 | */ 282 | private void ackError(ChannelHandlerContext ctx) { 283 | ctx.write("-\r\n"); 284 | } 285 | 286 | /** 287 | * Sends a retry ack to the client. 288 | * 289 | * @param ctx 290 | * Client connection. 291 | */ 292 | @SuppressWarnings("unused") 293 | private void ackRetry(ChannelHandlerContext ctx) { 294 | ctx.write("%\r\n"); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/ThreadWrapper.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | final class ThreadWrapper extends Thread { 4 | private final Job job; 5 | 6 | public ThreadWrapper(Job job) { 7 | super(job); 8 | this.job = job; 9 | } 10 | 11 | public Job getJob() { 12 | return job; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/TimeSlots.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | final class TimeSlots { 4 | 5 | private final boolean[] slots = new boolean[16]; 6 | private int lastSlotIndex = -1; 7 | 8 | /** 9 | * Sets active slots based on string representation. 10 | * 11 | * @param s 12 | * String representation of active slots. 13 | */ 14 | public synchronized void setSlots(String s) { 15 | // Reset all to false (instead of creating a new array) 16 | for (int i = 0; i < slots.length; ++i) { 17 | slots[i] = false; 18 | } 19 | 20 | for (int i = 0; i < s.length(); ++i) { 21 | int idx = Character.digit(s.charAt(i), 16); 22 | slots[idx] = true; 23 | } 24 | } 25 | 26 | /** 27 | * Checks if slot is allowed and counts how many active slots are in a row. 28 | * 29 | * @param cs 30 | * Slot to check. 31 | * @return Number of active slots. 32 | */ 33 | public synchronized int getCount(char cs) { 34 | return getCount(Character.digit(cs, 16)); 35 | } 36 | 37 | /** 38 | * Checks if slot is allowed and counts how many active slots are in a row. 39 | * 40 | * @param slot 41 | * Slot to check. 42 | * @return Number of active slots. 43 | */ 44 | public synchronized int getCount(int slot) { 45 | int count = 0; 46 | 47 | for (int i = slot; slots[i % 16] && (i < slot + 16); ++i) { 48 | ++count; 49 | } 50 | 51 | return count; 52 | } 53 | 54 | /** 55 | * Gets active slots as a string. 56 | * 57 | * @return String containing active slot indices. 58 | */ 59 | public synchronized String getSlots() { 60 | StringBuilder sb = new StringBuilder(); 61 | 62 | for (int i = 0; i < slots.length; ++i) { 63 | if (slots[i]) { 64 | sb.append(String.format("%1x", i)); 65 | } 66 | } 67 | 68 | return sb.toString(); 69 | } 70 | 71 | /** 72 | * Gets the value of a slot. 73 | * 74 | * @param index 75 | * Slot index (smaller than 16). 76 | * @return Status of the slot at the given index. 77 | */ 78 | public synchronized boolean get(int index) { 79 | return slots[index % 16]; 80 | } 81 | 82 | /** 83 | * Checks if the current slot is allowed. 84 | * 85 | * @param time 86 | * Time 87 | * @return True if the slot is allowed. 88 | */ 89 | public boolean isAllowed(int time) { 90 | return get(getIndex(time)); 91 | } 92 | 93 | /** 94 | * Checks if the slot has changed. 95 | * 96 | * @param time 97 | * Current time 98 | * @return True if the given slot number is the last slot. 99 | */ 100 | public synchronized boolean hasChanged(int time) { 101 | int slot = getIndex(time); 102 | if (lastSlotIndex == slot) { 103 | return false; 104 | } else { 105 | lastSlotIndex = slot; 106 | return true; 107 | } 108 | } 109 | 110 | /** 111 | * Cheks if the next slot will be active. 112 | * 113 | * @param time 114 | * Time 115 | * @return True if the next slot will be active. 116 | */ 117 | public synchronized boolean isNextAllowed(int time) { 118 | return get(getCurrent(time) + 1); 119 | } 120 | 121 | /** 122 | * Gets the current slot for the given time value. 123 | * 124 | * @param time 125 | * Time value 126 | * @return Current slot as hex number. 127 | */ 128 | public static char getCurrent(int time) { 129 | return Character.forDigit(getIndex(time), 16); 130 | } 131 | 132 | /** 133 | * Gets the current slot index for the given time value. 134 | * 135 | * @param time 136 | * Time value. 137 | * @return Slot index. 138 | */ 139 | public static int getIndex(int time) { 140 | // time (in 0.1s), time per slot 6.4 s = 64 * 0.1s 141 | // % 16 to warp around complete minutes, as there are 16 timeslots 142 | // avaliable. 143 | 144 | // **** IMPORTANT **** 145 | 146 | // This means 16 timeslots need 102.4 seconds, not 60. 147 | return ((int) (time / 64)) % 16; 148 | } 149 | 150 | public static int getNextIndex(int time) { 151 | return (getIndex(time) + 1) % 16; 152 | } 153 | 154 | public static int getStartTimeForSlot(int slot, int time) { 155 | return ((time % 1024) + (slot * 64)); 156 | } 157 | 158 | public static int getEndTimeForSlot(int slot, int time) { 159 | return (getStartTimeForSlot(slot, time) + 1024); 160 | } 161 | 162 | public static int getTimeToNextSlot(int time) { 163 | int nextSlot = (getCurrent(time) + 1) % 16; 164 | return (getStartTimeForSlot(nextSlot, time) - time); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/Transmitter.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager; 2 | 3 | import java.util.List; 4 | 5 | public interface Transmitter extends AutoCloseable { 6 | 7 | /** 8 | * Initializes the transmitter. 9 | * 10 | * @param config 11 | * Handle to the configuration file. 12 | * @throws Exception 13 | * If an error occurred during initialization. 14 | */ 15 | void init(Configuration config) throws Exception; 16 | 17 | /** 18 | * Encodes the code words into a raw byte array. 19 | * 20 | * @param data 21 | * Code words to encode. 22 | * @return Byte array containing the encoded data. 23 | * @throws Exception 24 | * If an error occurred while encoding the data. 25 | */ 26 | byte[] encode(List data) throws Exception; 27 | 28 | /** 29 | * Sends the encoded data over the air. 30 | * 31 | * @param data 32 | * Data to send. 33 | * @throws Exception 34 | * If an error occurred while sending the data. 35 | */ 36 | void send(byte[] data) throws Exception; 37 | } 38 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/sdr/AudioEncoder.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager.sdr; 2 | 3 | import java.util.List; 4 | 5 | import javax.sound.sampled.AudioFormat; 6 | import javax.sound.sampled.AudioSystem; 7 | import javax.sound.sampled.Clip; 8 | import javax.sound.sampled.LineEvent; 9 | import javax.sound.sampled.Mixer; 10 | 11 | /** 12 | * This class contains the audio encoder that encodes code words into an audio 13 | * clip which can be played by a sound card. 14 | */ 15 | final class AudioEncoder { 16 | // 0-2 = begin, 4-36 = constant, 38-39 = end 17 | private static final AudioFormat af48000 = new AudioFormat(48000, 16, 1, true, false); 18 | private static final float[] bitChange = { -0.9f, -0.7f, 0.0f, 0.7f, 0.9f }; 19 | private Mixer.Info device = null; 20 | private float correction = 0.0f; 21 | private Object playMutex = new Object(); 22 | 23 | /** 24 | * Constructs a new audio encoder using the given sound device. 25 | * 26 | * @param soundDevice 27 | * Name of the sound device to use. This must match the value 28 | * returned by {@link Mixer.Info#getName() getName}. 29 | */ 30 | public AudioEncoder(String soundDevice) { 31 | Mixer.Info[] soundDevices = AudioSystem.getMixerInfo(); 32 | for (Mixer.Info device : soundDevices) { 33 | if (device.getName().equalsIgnoreCase(soundDevice)) { 34 | this.device = device; 35 | break; 36 | } 37 | } 38 | 39 | if (device == null) { 40 | throw new IllegalArgumentException("Sound device does not exist."); 41 | } 42 | } 43 | 44 | /** 45 | * Gets the correction factor. 46 | * 47 | * @return Correction factor. 48 | */ 49 | public float getCorrection() { 50 | return correction; 51 | } 52 | 53 | /** 54 | * Sets the correction factor. 55 | * 56 | * @param correction 57 | */ 58 | public void setCorrection(float correction) { 59 | this.correction = correction; 60 | } 61 | 62 | /** 63 | * Encodes a list of code words into a byte array that can be send to the 64 | * soundcard. 65 | * 66 | * @param data 67 | * List of code words 68 | * @return Byte array containing the encoded data. 69 | */ 70 | public byte[] encode(List data) { 71 | return encode(toByteArray(data), correction); 72 | } 73 | 74 | /** 75 | * Plays the encoded data via the sound device. 76 | * 77 | * @param data 78 | * Data to play. 79 | * @throws Exception 80 | * If an error occurred. 81 | */ 82 | public void play(byte[] data) throws Exception { 83 | try (Clip c = AudioSystem.getClip(device)) { 84 | // auskommentieren, falls Downsampling verwendet werden soll 85 | c.open(af48000, data, 0, data.length); 86 | c.addLineListener((e) -> { 87 | if (e.getType() == LineEvent.Type.STOP) { 88 | try { 89 | c.close(); 90 | e.getLine().close(); 91 | } finally { 92 | synchronized (playMutex) { 93 | playMutex.notify(); 94 | } 95 | } 96 | } 97 | }); 98 | 99 | c.start(); 100 | c.loop(0); 101 | 102 | try { 103 | synchronized (playMutex) { 104 | playMutex.wait(); 105 | } 106 | } catch (InterruptedException ex) { 107 | throw ex; 108 | } 109 | } 110 | } 111 | 112 | private static byte[] encode(byte[] inputData, float correction) { 113 | byte sample_size = (byte) (af48000.getSampleSizeInBits() / 8); 114 | // 100 extra bytes to get the end data to be sent 115 | byte[] data = new byte[40 * sample_size * inputData.length * 8 + 100]; 116 | int max = (int) Math.pow(2, af48000.getSampleSizeInBits()) / 2 - 1; 117 | 118 | boolean lastHigh = false; 119 | int value = 0; 120 | 121 | for (int i = 0; i < inputData.length; i++) { 122 | // 0b1000 0000 Bit selection mask 123 | int comp = 128; 124 | 125 | // one byte containing 8 data bits to encode 126 | for (int j = 0; j < 8; j++) { 127 | // j = Bit index in current byte of input data 128 | 129 | // one bit 130 | // get index, which tells the start sample in audio sample array 131 | int index = (i * 8 + j) * 40; 132 | 133 | // select current bit high or low and compare with last 134 | boolean high = ((inputData[i] & comp) == comp); 135 | 136 | // System.out.println("Aktuelles Byte besthend aus 8 Bits" + 137 | // inputData[i]); 138 | // System.out.println("Aktuelles Bit " + high); 139 | 140 | boolean same = (high == lastHigh); 141 | lastHigh = high; 142 | 143 | // correction factor (for high to low/low to high and high or 144 | // low) 145 | // first 576 bits = praeembel 146 | // if ((high) || (i < 576)) { int f = 1} else { int f = -1} 147 | 148 | // Entweder i < 576 (Präambel), dann immer 1, 149 | // oder aktueller Bit-Wert ist in f, Werte 1 (=High) oder -1 150 | // (=Low) 151 | // int f = high || i < 576 ? 1 : -1; 152 | 153 | // geändert am 12.09.; fixt Ausgabe 154 | int f = high ? 1 : -1; 155 | 156 | // first 3 bits 157 | if (index == 0) { 158 | for (int l = 0; l <= 2; l++) { 159 | value = (int) ((int) (bitChange[2 + l] * max) * f * correction); 160 | 161 | // convert value to bytes 162 | for (int c = 0; c < sample_size; c++) { 163 | byte sample_byte = (byte) ((value >> (8 * c)) & 0xff); 164 | data[(index + l) * sample_size + c] = sample_byte; 165 | } 166 | } 167 | } else { 168 | // other bits 169 | if (same) { 170 | for (int l = 0; l < 5; ++l) { 171 | value = (int) (f * max * correction); 172 | 173 | for (int c = 0; c < sample_size; ++c) { 174 | byte sample_byte = (byte) ((value >> (8 * c)) & 0xff); 175 | data[(index - 2 + l) * sample_size + c] = sample_byte; 176 | } 177 | } 178 | } else { 179 | for (int l = 0; l < 5; ++l) { 180 | value = (int) (f * bitChange[l] * max * correction); 181 | 182 | for (int c = 0; c < sample_size; ++c) { 183 | byte sample_byte = (byte) ((value >> (8 * c)) & 0xff); 184 | data[(index - 2 + l) * sample_size + c] = sample_byte; 185 | } 186 | } 187 | } 188 | } 189 | 190 | for (int k = 3; k <= 37; k++) { 191 | // constant value 192 | value = (int) (f * max * correction); 193 | 194 | // convert value to bytes 195 | for (int c = 0; c < sample_size; c++) { 196 | byte sample_byte = (byte) ((value >> (8 * c)) & 0xff); 197 | data[(index + k) * sample_size + c] = sample_byte; 198 | } 199 | } 200 | 201 | // last 2 bit out of 40 202 | if (i == inputData.length - 1 && j == 7) { 203 | // end 204 | for (int l = 0; l <= 1; l++) { 205 | value = (int) ((int) (bitChange[l] * max) * -f * correction); 206 | 207 | // convert value to bytes 208 | for (int c = 0; c < sample_size; c++) { 209 | byte sample_byte = (byte) ((value >> (8 * c)) & 0xff); 210 | data[(index + 38 + l) * sample_size + c] = sample_byte; 211 | } 212 | } 213 | } 214 | 215 | // shift bit selection mask 1 bit right to select the 216 | // next bit in the following loop 217 | comp /= 2; 218 | } 219 | } 220 | 221 | return data; 222 | } 223 | 224 | /** 225 | * Converts the integer list into a byte array. 226 | * 227 | * @param data 228 | * Integer list 229 | * @return Byte array containing the integer data. 230 | */ 231 | private static byte[] toByteArray(List data) { 232 | byte[] byteData = new byte[data.size() * 4]; 233 | 234 | for (int i = 0; i < data.size(); i++) { 235 | int value = data.get(i); 236 | 237 | byteData[i * 4] = (byte) (value >>> 24); 238 | byteData[i * 4 + 1] = (byte) (value >>> 16); 239 | byteData[i * 4 + 2] = (byte) (value >>> 8); 240 | byteData[i * 4 + 3] = (byte) value; 241 | } 242 | 243 | return byteData; 244 | } 245 | } -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/sdr/GpioPortComm.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager.sdr; 2 | 3 | import com.pi4j.io.gpio.GpioController; 4 | import com.pi4j.io.gpio.GpioFactory; 5 | import com.pi4j.io.gpio.GpioPinDigitalOutput; 6 | import com.pi4j.io.gpio.Pin; 7 | import com.pi4j.io.gpio.PinState; 8 | import com.pi4j.io.gpio.RaspiPin; 9 | 10 | /** 11 | * GPIO controller implementation used by the RasPager SDR transmitter. 12 | * 13 | * @author Philipp Thiel 14 | */ 15 | final class GpioPortComm { 16 | private GpioController gpio; 17 | private GpioPinDigitalOutput gpioPin = null; 18 | private boolean invert = false; 19 | private boolean curOn = true; 20 | 21 | /** 22 | * Creates a new GPIO controller. 23 | * 24 | * @param pinName 25 | * Name of the GPIO pin to use. 26 | * @param invert 27 | * Invert pin behavior. 28 | */ 29 | public GpioPortComm(String pinName, boolean invert) { 30 | this.invert = invert; 31 | gpio = GpioFactory.getInstance(); 32 | 33 | Pin pin = RaspiPin.getPinByName(pinName); 34 | gpioPin = gpio.provisionDigitalOutputPin(pin, "FunkrufSlave", PinState.LOW); 35 | gpioPin.setShutdownOptions(true, PinState.LOW); 36 | } 37 | 38 | /** 39 | * Enables the GPIO pin. 40 | * 41 | * @throws IllegalStateException 42 | * If the GPIO pin is not initialized. 43 | */ 44 | public void setOn() { 45 | setStatus(!invert); 46 | } 47 | 48 | /** 49 | * Disables the GPIO pin. 50 | * 51 | * @throws IllegalStateException 52 | * If the GPIO pin is not initialized. 53 | */ 54 | public void setOff() { 55 | setStatus(invert); 56 | } 57 | 58 | /** 59 | * Sets the pin status. 60 | * 61 | * @param on 62 | * Enable or disable the pin. 63 | * @throws IllegalStateException 64 | * If the GPIO pin is not initialized. 65 | */ 66 | private void setStatus(boolean on) { 67 | if (gpioPin == null) { 68 | throw new IllegalStateException("GPIO pin not initialized"); 69 | } 70 | 71 | if (on && !curOn) { 72 | gpioPin.low(); 73 | curOn = true; 74 | } else if (!on && curOn) { 75 | gpioPin.high(); 76 | curOn = false; 77 | } 78 | } 79 | 80 | /** 81 | * Closes the GPIO pin. 82 | */ 83 | public void close() { 84 | if (gpio != null) { 85 | gpioPin.low(); 86 | gpio.shutdown(); 87 | gpio.unprovisionPin(gpioPin); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/sdr/SDRTransmitter.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager.sdr; 2 | 3 | import java.util.List; 4 | import java.util.logging.Level; 5 | import java.util.logging.Logger; 6 | 7 | import de.rwth_aachen.afu.raspager.Configuration; 8 | import de.rwth_aachen.afu.raspager.Transmitter; 9 | 10 | /** 11 | * RasPager SDR transmitter implementation. 12 | * 13 | * @author Philipp Thiel 14 | */ 15 | public final class SDRTransmitter implements Transmitter { 16 | private static final Logger log = Logger.getLogger(SDRTransmitter.class.getName()); 17 | private final Object lockObj = new Object(); 18 | private AudioEncoder encoder; 19 | private SerialPortComm serial; 20 | private GpioPortComm gpio; 21 | private int txDelay = 0; 22 | 23 | @Override 24 | public void close() throws Exception { 25 | synchronized (lockObj) { 26 | try { 27 | if (serial != null) { 28 | serial.close(); 29 | serial = null; 30 | } 31 | } catch (Throwable t) { 32 | log.log(Level.SEVERE, "Failed to close serial port.", t); 33 | } 34 | 35 | try { 36 | if (gpio != null) { 37 | gpio.close(); 38 | gpio = null; 39 | } 40 | } catch (Throwable t) { 41 | log.log(Level.SEVERE, "Failed to close GPIO port.", t); 42 | } 43 | 44 | encoder = null; 45 | } 46 | } 47 | 48 | @Override 49 | public void init(Configuration config) throws Exception { 50 | synchronized (lockObj) { 51 | close(); 52 | 53 | txDelay = config.getInt("txDelay", 0); 54 | boolean invert = config.getBoolean("invert", false); 55 | 56 | if (config.getBoolean("serial.use", false)) { 57 | int pin = SerialPortComm.getPinNumber(config.getString("serial.pin")); 58 | serial = new SerialPortComm(config.getString("serial.port"), pin, invert); 59 | } 60 | 61 | if (config.getBoolean("gpio.use", true)) { 62 | gpio = new GpioPortComm(config.getString("gpio.pin"), invert); 63 | } 64 | 65 | encoder = new AudioEncoder(config.getString("sdr.device")); 66 | encoder.setCorrection(config.getFloat("sdr.correction", 0.0f)); 67 | } 68 | } 69 | 70 | @Override 71 | public byte[] encode(List data) throws Exception { 72 | synchronized (lockObj) { 73 | if (encoder != null) { 74 | return encoder.encode(data); 75 | } else { 76 | throw new IllegalStateException("Encoder not initialized."); 77 | } 78 | } 79 | } 80 | 81 | @Override 82 | public void send(byte[] data) throws Exception { 83 | synchronized (lockObj) { 84 | if (serial == null && gpio == null) { 85 | throw new IllegalStateException("Not initialized"); 86 | } 87 | 88 | try { 89 | enable(); 90 | 91 | if (txDelay > 0) { 92 | try { 93 | Thread.sleep(txDelay); 94 | } catch (Throwable t) { 95 | log.log(Level.SEVERE, "Failed to wait for TX delay.", t); 96 | } 97 | } 98 | 99 | encoder.play(data); 100 | } finally { 101 | disable(); 102 | } 103 | } 104 | } 105 | 106 | private void enable() { 107 | try { 108 | if (serial != null) { 109 | log.fine("Enabling serial pin."); 110 | serial.setOn(); 111 | } 112 | } catch (Throwable t) { 113 | log.log(Level.SEVERE, "Failed to enable serial port.", t); 114 | throw t; 115 | } 116 | 117 | try { 118 | if (gpio != null) { 119 | log.fine("Enabling GPIO pin."); 120 | gpio.setOn(); 121 | } 122 | } catch (Throwable t) { 123 | log.log(Level.SEVERE, "Failed to enable GPIO port.", t); 124 | throw t; 125 | } 126 | } 127 | 128 | private void disable() { 129 | try { 130 | if (serial != null) { 131 | log.fine("Disabling serial pin."); 132 | serial.setOff(); 133 | } 134 | } catch (Throwable t) { 135 | log.log(Level.SEVERE, "Failed to disable serial port.", t); 136 | } 137 | 138 | try { 139 | if (gpio != null) { 140 | log.fine("Disabling GPIO pin."); 141 | gpio.setOff(); 142 | } 143 | } catch (Throwable t) { 144 | log.log(Level.SEVERE, "Failed to disable GPIO port.", t); 145 | } 146 | } 147 | 148 | public void setCorrection(float correction) { 149 | synchronized (lockObj) { 150 | if (encoder != null) { 151 | encoder.setCorrection(correction); 152 | } 153 | } 154 | } 155 | 156 | public float getCorrection() { 157 | synchronized (lockObj) { 158 | if (encoder != null) { 159 | return encoder.getCorrection(); 160 | } else { 161 | return 0.0f; 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/java/de/rwth_aachen/afu/raspager/sdr/SerialPortComm.java: -------------------------------------------------------------------------------- 1 | package de.rwth_aachen.afu.raspager.sdr; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Enumeration; 5 | import java.util.List; 6 | 7 | import gnu.io.CommPortIdentifier; 8 | import gnu.io.NoSuchPortException; 9 | import gnu.io.PortInUseException; 10 | import gnu.io.SerialPort; 11 | import gnu.io.UnsupportedCommOperationException; 12 | 13 | /** 14 | * Serial port controller used by the SDR transmitter. 15 | * 16 | * @author Philipp Thiel 17 | */ 18 | public final class SerialPortComm { 19 | public static final int DTR = 0; 20 | public static final int RTS = 1; 21 | 22 | private SerialPort serialPort = null; 23 | private int pin = DTR; 24 | private boolean invert = false; 25 | 26 | /** 27 | * Gets the pin number for a given name. 28 | * 29 | * @param pin 30 | * Pin name 31 | * @return Pin number the corresponds to the given name. 32 | * @throws IllegalArgumentException 33 | * If the given name does not match a pin. 34 | */ 35 | public static int getPinNumber(String pin) { 36 | if ("DTR".equalsIgnoreCase(pin)) { 37 | return DTR; 38 | } else if ("RTS".equalsIgnoreCase(pin)) { 39 | return RTS; 40 | } else { 41 | throw new IllegalArgumentException("Invalid pin."); 42 | } 43 | } 44 | 45 | /** 46 | * Converts the pin number to a name. 47 | * 48 | * @param pin 49 | * Pin number to convert. 50 | * @return Pin name for the given number. 51 | * @throws IllegalArgumentException 52 | * If the pin number is invalid. 53 | */ 54 | public static String getPinName(int pin) { 55 | switch (pin) { 56 | case DTR: 57 | return "DTR"; 58 | case RTS: 59 | return "RTS"; 60 | default: 61 | throw new IllegalArgumentException("Invalid pin number."); 62 | } 63 | } 64 | 65 | /** 66 | * Construct a new serial port controller. 67 | * 68 | * @param portName 69 | * Serial port to use. 70 | * @param pin 71 | * Pin number to use (DTR or RTS). 72 | * @param invert 73 | * Invert 74 | * @throws NoSuchPortException 75 | * If the given port does not exist. 76 | * @throws PortInUseException 77 | * If the given port is already in use. 78 | * @throws UnsupportedCommOperationException 79 | * If rxtx is not happy. 80 | */ 81 | public SerialPortComm(String portName, int pin, boolean invert) 82 | throws NoSuchPortException, PortInUseException, UnsupportedCommOperationException { 83 | this.pin = pin; 84 | this.invert = invert; 85 | 86 | CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName); 87 | if (portIdentifier.isCurrentlyOwned()) { 88 | throw new PortInUseException(); 89 | } else { 90 | serialPort = (SerialPort) portIdentifier.open("FunkrufSlave", 2000); 91 | serialPort.setSerialPortParams(115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, 92 | SerialPort.PARITY_NONE); 93 | } 94 | 95 | setOff(); 96 | } 97 | 98 | /** 99 | * Enable the pin. 100 | * 101 | * @throws IllegalStateException 102 | * If the serial port is not initialized. 103 | */ 104 | public void setOn() { 105 | setStatus(!this.invert); 106 | } 107 | 108 | /** 109 | * Disables the pin. 110 | * 111 | * @throws IllegalStateException 112 | * If the serial port is not initialized. 113 | */ 114 | public void setOff() { 115 | setStatus(this.invert); 116 | } 117 | 118 | /** 119 | * Sets the pin status. 120 | * 121 | * @param on 122 | * Enable or disable the pin. 123 | * @throws IllegalStateException 124 | * If the serial port is not initialized. 125 | */ 126 | private void setStatus(boolean on) { 127 | if (serialPort == null) { 128 | throw new IllegalStateException("Serial port is not initialized."); 129 | } 130 | 131 | switch (this.pin) { 132 | case DTR: 133 | if (serialPort.isDTR() != on) { 134 | serialPort.setDTR(on); 135 | } 136 | break; 137 | case RTS: 138 | if (serialPort.isRTS() != on) { 139 | serialPort.setRTS(on); 140 | } 141 | break; 142 | } 143 | } 144 | 145 | /** 146 | * Closes the serial port. 147 | */ 148 | public void close() { 149 | if (serialPort != null) { 150 | setOff(); 151 | serialPort.close(); 152 | serialPort = null; 153 | } 154 | } 155 | 156 | /** 157 | * Gets a list of available serial ports. 158 | * 159 | * @return List of serial ports. 160 | */ 161 | public static List getPorts() { 162 | List list = new ArrayList(); 163 | 164 | @SuppressWarnings("unchecked") 165 | Enumeration portEnum = CommPortIdentifier.getPortIdentifiers(); 166 | while (portEnum.hasMoreElements()) { 167 | CommPortIdentifier portIdentifier = portEnum.nextElement(); 168 | 169 | // if port is a serial port, add name to list 170 | if (portIdentifier.getPortType() == CommPortIdentifier.PORT_SERIAL) { 171 | list.add(portIdentifier.getName()); 172 | } 173 | } 174 | 175 | return list; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /raspager-sdr/src/main/resources/MainWindow.properties: -------------------------------------------------------------------------------- 1 | addMasterFailText = The master server is already in the list. 2 | addMasterFailTitle = Add Master 3 | applyButton = Apply 4 | askQuitText = The server is currently running. Do you really want to quit? 5 | askQuitTitle = Quit 6 | btnGpioPins = GPIO pins 7 | cfgRunningText = In order to apply the configuration the server must be restarted. Restart now? 8 | cfgRunningTitle = Save Config 9 | configurationPanel = Configuration 10 | correctionLabel = Correction 11 | delMasterText = Delete the selected master? 12 | delMasterTitle = Delete Master 13 | failedConfigText = The configuration could not be saved. 14 | failedConfigTitle = Save failed 15 | gpioPanel = GPIO pin (Raspi) 16 | invalidConfigText = The selected configuration file could not be loaded. 17 | invalidConfigTitle = Invalid Configuration 18 | invert = Invert 19 | itemDeactivated = Deactivated 20 | loadButton = Load 21 | masterAdd = Add 22 | masterListLabel = Masters 23 | masterPanel = New Master 24 | masterRemove = Remove 25 | noSearchAddress = Search address is missing. 26 | pttPanel = PTT Control 27 | saveButton = Save 28 | searchAddressLabel = Skyper Address 29 | searchErrorTitle = Search 30 | searchLabel = Search Run 31 | searchRunningText = In order to perform a search the server must be stopped. Stop now? 32 | searchRunningTitle = Search 33 | searchStart = Start 34 | searchStepLabel = Step size 35 | searchStop = Stop 36 | serialDelayLabel = Delay 37 | serialPanel = Serial Port 38 | slotDisplayLabel = Slots 39 | soundDeviceLabel = Sound Device 40 | startButtonStart = Start Server 41 | startButtonStop = Stop Server 42 | statusDisplayCon = Connected 43 | statusDisplayDis = Disconnected 44 | statusDisplayLabel = Status 45 | trayMenu = RasPager 46 | trayMenuShow = Show -------------------------------------------------------------------------------- /raspager-sdr/src/main/resources/MainWindow_de_DE.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwth-afu/SDRPager/f9c0486dd5f89fb896445c0b016386e28f959667/raspager-sdr/src/main/resources/MainWindow_de_DE.properties -------------------------------------------------------------------------------- /raspager-sdr/src/main/resources/MainWindow_es_ES.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwth-afu/SDRPager/f9c0486dd5f89fb896445c0b016386e28f959667/raspager-sdr/src/main/resources/MainWindow_es_ES.properties -------------------------------------------------------------------------------- /raspager-sdr/src/main/resources/pi_gpio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwth-afu/SDRPager/f9c0486dd5f89fb896445c0b016386e28f959667/raspager-sdr/src/main/resources/pi_gpio.png --------------------------------------------------------------------------------