├── 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
--------------------------------------------------------------------------------