├── README.md ├── arduino └── etch-a-sdr.ino ├── chrome.sh ├── clear_etch.sh ├── controls.sh ├── etch-a-node ├── app.js ├── controls.py ├── jade │ └── etch.jade ├── package.json └── public │ └── assets │ ├── css │ ├── normalize.css │ └── style.css │ └── js │ ├── jquery-ui.js │ ├── jquery.js │ ├── node.js │ └── script.js ├── etch_controls.sh ├── etch_start.py ├── gqrx.sh ├── gqrx_arduino.py ├── gqrx_mouse.sh ├── gqrx_start.py ├── images ├── etch-a-sdr.jpg ├── left_side.jpg └── right_side.jpg ├── reboot.sh ├── rebootct.txt ├── start.sh ├── start_chrome.sh ├── start_etch.sh └── startup /README.md: -------------------------------------------------------------------------------- 1 | # Etch-A-SDR 2 | 3 | The Etch-A-SDR is a Etch-A-Sketch and SDR all in one! Built with an Odroid C1, Teensy 3.1 and RTL-SDR. 4 | 5 | Demo: https://www.youtube.com/watch?v=1YYz0kW67oU 6 | 7 | Presentation at [Cyberspectrum SDR Meetup](http://www.meetup.com/Cyberspectrum/): https://www.youtube.com/watch?v=tG70c3Zadek 8 | 9 | Slides from Cyberspectrum Presentation: https://github.com/devnulling/etch-a-sdr-presentation 10 | 11 | ![etch-a-sdr](https://raw.githubusercontent.com/devnulling/etch-a-sdr/master/images/etch-a-sdr.jpg) 12 | ![etch-a-sdr](https://raw.githubusercontent.com/devnulling/etch-a-sdr/master/images/left_side.jpg) 13 | ![etch-a-sdr](https://raw.githubusercontent.com/devnulling/etch-a-sdr/master/images/right_side.jpg) 14 | -------------------------------------------------------------------------------- /arduino/etch-a-sdr.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define MPU6050_AUX_VDDIO 0x01 // R/W 5 | #define MPU6050_SMPLRT_DIV 0x19 // R/W 6 | #define MPU6050_CONFIG 0x1A // R/W 7 | #define MPU6050_GYRO_CONFIG 0x1B // R/W 8 | #define MPU6050_ACCEL_CONFIG 0x1C // R/W 9 | #define MPU6050_FF_THR 0x1D // R/W 10 | #define MPU6050_FF_DUR 0x1E // R/W 11 | #define MPU6050_MOT_THR 0x1F // R/W 12 | #define MPU6050_MOT_DUR 0x20 // R/W 13 | #define MPU6050_ZRMOT_THR 0x21 // R/W 14 | #define MPU6050_ZRMOT_DUR 0x22 // R/W 15 | #define MPU6050_FIFO_EN 0x23 // R/W 16 | #define MPU6050_I2C_MST_CTRL 0x24 // R/W 17 | #define MPU6050_I2C_SLV0_ADDR 0x25 // R/W 18 | #define MPU6050_I2C_SLV0_REG 0x26 // R/W 19 | #define MPU6050_I2C_SLV0_CTRL 0x27 // R/W 20 | #define MPU6050_I2C_SLV1_ADDR 0x28 // R/W 21 | #define MPU6050_I2C_SLV1_REG 0x29 // R/W 22 | #define MPU6050_I2C_SLV1_CTRL 0x2A // R/W 23 | #define MPU6050_I2C_SLV2_ADDR 0x2B // R/W 24 | #define MPU6050_I2C_SLV2_REG 0x2C // R/W 25 | #define MPU6050_I2C_SLV2_CTRL 0x2D // R/W 26 | #define MPU6050_I2C_SLV3_ADDR 0x2E // R/W 27 | #define MPU6050_I2C_SLV3_REG 0x2F // R/W 28 | #define MPU6050_I2C_SLV3_CTRL 0x30 // R/W 29 | #define MPU6050_I2C_SLV4_ADDR 0x31 // R/W 30 | #define MPU6050_I2C_SLV4_REG 0x32 // R/W 31 | #define MPU6050_I2C_SLV4_DO 0x33 // R/W 32 | #define MPU6050_I2C_SLV4_CTRL 0x34 // R/W 33 | #define MPU6050_I2C_SLV4_DI 0x35 // R 34 | #define MPU6050_I2C_MST_STATUS 0x36 // R 35 | #define MPU6050_INT_PIN_CFG 0x37 // R/W 36 | #define MPU6050_INT_ENABLE 0x38 // R/W 37 | #define MPU6050_INT_STATUS 0x3A // R 38 | #define MPU6050_ACCEL_XOUT_H 0x3B // R 39 | #define MPU6050_ACCEL_XOUT_L 0x3C // R 40 | #define MPU6050_ACCEL_YOUT_H 0x3D // R 41 | #define MPU6050_ACCEL_YOUT_L 0x3E // R 42 | #define MPU6050_ACCEL_ZOUT_H 0x3F // R 43 | #define MPU6050_ACCEL_ZOUT_L 0x40 // R 44 | #define MPU6050_TEMP_OUT_H 0x41 // R 45 | #define MPU6050_TEMP_OUT_L 0x42 // R 46 | #define MPU6050_GYRO_XOUT_H 0x43 // R 47 | #define MPU6050_GYRO_XOUT_L 0x44 // R 48 | #define MPU6050_GYRO_YOUT_H 0x45 // R 49 | #define MPU6050_GYRO_YOUT_L 0x46 // R 50 | #define MPU6050_GYRO_ZOUT_H 0x47 // R 51 | #define MPU6050_GYRO_ZOUT_L 0x48 // R 52 | #define MPU6050_EXT_SENS_DATA_00 0x49 // R 53 | #define MPU6050_EXT_SENS_DATA_01 0x4A // R 54 | #define MPU6050_EXT_SENS_DATA_02 0x4B // R 55 | #define MPU6050_EXT_SENS_DATA_03 0x4C // R 56 | #define MPU6050_EXT_SENS_DATA_04 0x4D // R 57 | #define MPU6050_EXT_SENS_DATA_05 0x4E // R 58 | #define MPU6050_EXT_SENS_DATA_06 0x4F // R 59 | #define MPU6050_EXT_SENS_DATA_07 0x50 // R 60 | #define MPU6050_EXT_SENS_DATA_08 0x51 // R 61 | #define MPU6050_EXT_SENS_DATA_09 0x52 // R 62 | #define MPU6050_EXT_SENS_DATA_10 0x53 // R 63 | #define MPU6050_EXT_SENS_DATA_11 0x54 // R 64 | #define MPU6050_EXT_SENS_DATA_12 0x55 // R 65 | #define MPU6050_EXT_SENS_DATA_13 0x56 // R 66 | #define MPU6050_EXT_SENS_DATA_14 0x57 // R 67 | #define MPU6050_EXT_SENS_DATA_15 0x58 // R 68 | #define MPU6050_EXT_SENS_DATA_16 0x59 // R 69 | #define MPU6050_EXT_SENS_DATA_17 0x5A // R 70 | #define MPU6050_EXT_SENS_DATA_18 0x5B // R 71 | #define MPU6050_EXT_SENS_DATA_19 0x5C // R 72 | #define MPU6050_EXT_SENS_DATA_20 0x5D // R 73 | #define MPU6050_EXT_SENS_DATA_21 0x5E // R 74 | #define MPU6050_EXT_SENS_DATA_22 0x5F // R 75 | #define MPU6050_EXT_SENS_DATA_23 0x60 // R 76 | #define MPU6050_MOT_DETECT_STATUS 0x61 // R 77 | #define MPU6050_I2C_SLV0_DO 0x63 // R/W 78 | #define MPU6050_I2C_SLV1_DO 0x64 // R/W 79 | #define MPU6050_I2C_SLV2_DO 0x65 // R/W 80 | #define MPU6050_I2C_SLV3_DO 0x66 // R/W 81 | #define MPU6050_I2C_MST_DELAY_CTRL 0x67 // R/W 82 | #define MPU6050_SIGNAL_PATH_RESET 0x68 // R/W 83 | #define MPU6050_MOT_DETECT_CTRL 0x69 // R/W 84 | #define MPU6050_USER_CTRL 0x6A // R/W 85 | #define MPU6050_PWR_MGMT_1 0x6B // R/W 86 | #define MPU6050_PWR_MGMT_2 0x6C // R/W 87 | #define MPU6050_FIFO_COUNTH 0x72 // R/W 88 | #define MPU6050_FIFO_COUNTL 0x73 // R/W 89 | #define MPU6050_FIFO_R_W 0x74 // R/W 90 | #define MPU6050_WHO_AM_I 0x75 // R 91 | 92 | 93 | // Defines for the bits, to be able to change 94 | // between bit number and binary definition. 95 | // By using the bit number, programming the sensor 96 | // is like programming the AVR microcontroller. 97 | // But instead of using "(1< positionRight) { 733 | Serial.print("RU "); 734 | 735 | } 736 | if (newRight < positionRight) { 737 | Serial.print("RD "); 738 | 739 | } 740 | Serial.print(newRight); 741 | Serial.println(); 742 | positionRight = newRight; 743 | } 744 | 745 | if (digitalRead(pinRightKnob) == LOW) { 746 | Serial.println("R0"); 747 | } 748 | if (digitalRead(pinLeftKnob) == LOW) { 749 | Serial.println("L0"); 750 | } 751 | if (digitalRead(pinRightButton1) == LOW) { 752 | Serial.println("R1"); 753 | } 754 | if (digitalRead(pinRightButton2) == LOW) { 755 | Serial.println("R2"); 756 | } 757 | if (digitalRead(pinRightButton3) == LOW) { 758 | Serial.println("R3"); 759 | } 760 | if (digitalRead(pinLeftButton1) == LOW) { 761 | Serial.println("L1"); 762 | } 763 | if (digitalRead(pinLeftButton2) == LOW) { 764 | Serial.println("L2"); 765 | } 766 | if (digitalRead(pintLeftButtonMode) == LOW) { 767 | Serial.println("L3"); 768 | } 769 | 770 | 771 | int error; 772 | double dT; 773 | accel_t_gyro_union accel_t_gyro; 774 | 775 | 776 | //Serial.println(F("")); 777 | //Serial.println(F("MPU-6050")); 778 | 779 | // Read the raw values. 780 | // Read 14 bytes at once, 781 | // containing acceleration, temperature and gyro. 782 | // With the default settings of the MPU-6050, 783 | // there is no filter enabled, and the values 784 | // are not very stable. 785 | error = MPU6050_read(MPU6050_ACCEL_XOUT_H, (uint8_t *) & accel_t_gyro, sizeof (accel_t_gyro)); 786 | 787 | 788 | // Serial.print(F("mpu_error = ")); 789 | // Serial.println(error, DEC); 790 | 791 | 792 | // Swap all high and low bytes. 793 | // After this, the registers values are swapped, 794 | // so the structure name like x_accel_l does no 795 | // longer contain the lower byte. 796 | uint8_t swap; 797 | #define SWAP(x,y) swap = x; x = y; y = swap 798 | 799 | SWAP(accel_t_gyro.reg.x_accel_h, accel_t_gyro.reg.x_accel_l); 800 | SWAP(accel_t_gyro.reg.y_accel_h, accel_t_gyro.reg.y_accel_l); 801 | SWAP(accel_t_gyro.reg.z_accel_h, accel_t_gyro.reg.z_accel_l); 802 | SWAP(accel_t_gyro.reg.t_h, accel_t_gyro.reg.t_l); 803 | SWAP(accel_t_gyro.reg.x_gyro_h, accel_t_gyro.reg.x_gyro_l); 804 | SWAP(accel_t_gyro.reg.y_gyro_h, accel_t_gyro.reg.y_gyro_l); 805 | SWAP(accel_t_gyro.reg.z_gyro_h, accel_t_gyro.reg.z_gyro_l); 806 | 807 | 808 | // Print the raw acceleration values 809 | 810 | Serial.print(F("MPU, ")); 811 | Serial.print(accel_t_gyro.value.x_accel, DEC); 812 | Serial.print(F(", ")); 813 | Serial.print(accel_t_gyro.value.y_accel, DEC); 814 | Serial.print(F(", ")); 815 | Serial.print(accel_t_gyro.value.z_accel, DEC); 816 | Serial.println(F("")); 817 | 818 | 819 | // The temperature sensor is -40 to +85 degrees Celsius. 820 | // It is a signed integer. 821 | // According to the datasheet: 822 | // 340 per degrees Celsius, -512 at 35 degrees. 823 | // At 0 degrees: -512 - (340 * 35) = -12412 824 | 825 | // Serial.print(F("mpu_temp: ")); 826 | // dT = ((double) accel_t_gyro.value.temperature + 12412.0) / 340.0; 827 | // Serial.print(dT, 3); 828 | // Serial.println(F("")); 829 | 830 | 831 | // Print the raw gyro values. 832 | 833 | // Serial.print(F("mpu_gyro: ")); 834 | // Serial.print(accel_t_gyro.value.x_gyro, DEC); 835 | // Serial.print(F(", ")); 836 | // Serial.print(accel_t_gyro.value.y_gyro, DEC); 837 | // Serial.print(F(", ")); 838 | // Serial.print(accel_t_gyro.value.z_gyro, DEC); 839 | // Serial.print(F(", ")); 840 | // Serial.println(F("")); 841 | 842 | //delay(200); 843 | delay(100); 844 | 845 | 846 | // if a character is sent from the serial monitor, 847 | // reset both back to zero. 848 | if (Serial.available()) { 849 | Serial.read(); 850 | Serial.println("Reset both knobs to zero"); 851 | knobLeft.write(0); 852 | knobRight.write(0); 853 | } 854 | } 855 | 856 | 857 | 858 | 859 | 860 | // -------------------------------------------------------- 861 | // MPU6050_read 862 | // 863 | // This is a common function to read multiple bytes 864 | // from an I2C device. 865 | // 866 | // It uses the boolean parameter for Wire.endTransMission() 867 | // to be able to hold or release the I2C-bus. 868 | // This is implemented in Arduino 1.0.1. 869 | // 870 | // Only this function is used to read. 871 | // There is no function for a single byte. 872 | // 873 | 874 | int MPU6050_read(int start, uint8_t *buffer, int size) { 875 | int i, n, error; 876 | 877 | Wire.beginTransmission(MPU6050_I2C_ADDRESS); 878 | n = Wire.write(start); 879 | if (n != 1) 880 | return (-10); 881 | 882 | n = Wire.endTransmission(false); // hold the I2C-bus 883 | if (n != 0) 884 | return (n); 885 | 886 | // Third parameter is true: relase I2C-bus after data is read. 887 | Wire.requestFrom(MPU6050_I2C_ADDRESS, size, true); 888 | i = 0; 889 | while (Wire.available() && i < size) { 890 | buffer[i++] = Wire.read(); 891 | } 892 | if (i != size) 893 | return (-11); 894 | 895 | return (0); // return : no error 896 | } 897 | 898 | 899 | // -------------------------------------------------------- 900 | // MPU6050_write 901 | // 902 | // This is a common function to write multiple bytes to an I2C device. 903 | // 904 | // If only a single register is written, 905 | // use the function MPU_6050_write_reg(). 906 | // 907 | // Parameters: 908 | // start : Start address, use a define for the register 909 | // pData : A pointer to the data to write. 910 | // size : The number of bytes to write. 911 | // 912 | // If only a single register is written, a pointer 913 | // to the data has to be used, and the size is 914 | // a single byte: 915 | // int data = 0; // the data to write 916 | // MPU6050_write (MPU6050_PWR_MGMT_1, &c, 1); 917 | // 918 | 919 | int MPU6050_write(int start, const uint8_t *pData, int size) { 920 | int n, error; 921 | 922 | Wire.beginTransmission(MPU6050_I2C_ADDRESS); 923 | n = Wire.write(start); // write the start address 924 | if (n != 1) 925 | return (-20); 926 | 927 | n = Wire.write(pData, size); // write data bytes 928 | if (n != size) 929 | return (-21); 930 | 931 | error = Wire.endTransmission(true); // release the I2C-bus 932 | if (error != 0) 933 | return (error); 934 | 935 | return (0); // return : no error 936 | } 937 | 938 | // -------------------------------------------------------- 939 | // MPU6050_write_reg 940 | // 941 | // An extra function to write a single register. 942 | // It is just a wrapper around the MPU_6050_write() 943 | // function, and it is only a convenient function 944 | // to make it easier to write a single register. 945 | // 946 | 947 | int MPU6050_write_reg(int reg, uint8_t data) { 948 | int error; 949 | 950 | error = MPU6050_write(reg, &data, 1); 951 | 952 | return (error); 953 | } 954 | -------------------------------------------------------------------------------- /chrome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' /home/odroid/.config/chromium/Default/Preferences 3 | chromium-browser --incognito --disable-restore-session-state --disable-sync --app="http://localhost:7200/" 4 | -------------------------------------------------------------------------------- /clear_etch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl "http://localhost:7200/post?c=etch&m=c" 3 | -------------------------------------------------------------------------------- /controls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sleep 30 3 | sudo python /home/odroid/gqrx_arduino.py & 4 | -------------------------------------------------------------------------------- /etch-a-node/app.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var bodyParser = require('body-parser'); 3 | var app = express(); 4 | var port = 7200; 5 | var fs = require("fs"); 6 | var redis = require("redis"); 7 | var channel = []; 8 | var channels = ['etch']; 9 | 10 | app.use(bodyParser.json()); // to support JSON-encoded bodies 11 | app.use(bodyParser.urlencoded({// to support URL-encoded bodies 12 | extended: true 13 | })); 14 | 15 | var http = require('http'); 16 | http.createServer(function (req, res) { 17 | }); 18 | 19 | app.set('views', __dirname + '/jade'); 20 | app.set('view engine', "jade"); 21 | app.engine('jade', require('jade').__express); 22 | 23 | app.get("/", function (req, res) { 24 | res.render("etch"); 25 | }); 26 | app.get("/post", function (req, res) { 27 | var channel = req.query.c, 28 | msg = req.query.m; 29 | 30 | publishMsg(channel, msg); 31 | var status = 200; 32 | var body = 'OK'; 33 | res.status(status).send(body); 34 | }); 35 | 36 | app.use(express.static(__dirname + '/public')); 37 | var io = require('socket.io').listen(app.listen(port)); 38 | console.log("Listening on port " + port); 39 | 40 | for (var i in channels) { 41 | channel[i] = redis.createClient(); 42 | channel[i].subscribe(channels[i]); 43 | channel[i].on("message", function (channel, message) { 44 | console.log("client channel recieve from channel : %s, the message : %s", channel, message); 45 | io.sockets.emit('message', {message: message, channel: channel}); 46 | }); 47 | } 48 | 49 | function publishMsg(channel, message) { 50 | var redisClient = redis.createClient(); 51 | redisClient.publish(channel, message); 52 | } 53 | -------------------------------------------------------------------------------- /etch-a-node/controls.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import requests 3 | from time import sleep 4 | import redis 5 | import os 6 | import subprocess 7 | import time 8 | 9 | sleep(15) 10 | 11 | ser = serial.Serial('/dev/ttyACM0', 115200) 12 | r = redis.StrictRedis(host='localhost', port=6379, db=0) 13 | 14 | r0 = "R0" 15 | l0 = "L0" 16 | 17 | r1 = "R1" 18 | r2 = "R2" 19 | r3 = "R3" 20 | 21 | l1 = "L1" 22 | l2 = "L2" 23 | l3 = "L3" 24 | 25 | l_up = "LU" 26 | l_down = "LD" 27 | 28 | r_up = "RU" 29 | r_down = "RD" 30 | 31 | mp = "MP" 32 | 33 | milli_time = lambda: int(round(time.time() * 1000)) 34 | last_clear = milli_time() 35 | current_time = milli_time() 36 | 37 | class L(list): 38 | def append(self, item): 39 | list.append(self, int(item)) 40 | if len(self) > 5: self[:1]=[] 41 | 42 | def test(list): 43 | global last_clear 44 | v = [list[i+1]-list[i] for i in range(len(list)-1)] 45 | for item in v: 46 | if item > 10000: 47 | print "shake shake shake" 48 | current_time = milli_time() 49 | print current_time 50 | if last_clear + 15000 <= current_time: 51 | print "clearring" 52 | last_clear = milli_time() 53 | clearEtch() 54 | 55 | def clearEtch(): 56 | r = requests.get('http://localhost:7200/post?c=etch&m=c') 57 | 58 | mpus = L() 59 | 60 | while True: 61 | newline = ser.readline() 62 | newline = newline.rstrip() 63 | 64 | key = newline[:2] 65 | print key 66 | 67 | if key == mp: 68 | mpu = newline.replace(' ','').split(',') 69 | print "MPU: %s" % mpu[1] 70 | mpus.append(mpu[1]) 71 | 72 | test(mpus) 73 | 74 | elif key == l_up: 75 | print 'left up' 76 | r.publish('etch', 'u') 77 | 78 | elif key == l_down: 79 | print 'left down' 80 | r.publish('etch', 'd') 81 | 82 | elif key == r_up: 83 | print 'right up' 84 | r.publish('etch', 'l') 85 | 86 | elif key == r_down: 87 | print 'right down' 88 | r.publish('etch', 'r') 89 | 90 | elif key == l1: 91 | print 'left up' 92 | r.publish('etch', 'U') 93 | 94 | elif key == l2: 95 | print 'left down' 96 | r.publish('etch', 'D') 97 | 98 | elif key == l3: 99 | subprocess.Popen("/home/odroid/reboot.sh", shell=False) 100 | sleep(1) 101 | 102 | elif key == r1: 103 | print 'right up' 104 | r.publish('etch', 'L') 105 | 106 | elif key == r2: 107 | print 'right down' 108 | r.publish('etch', 'R') 109 | elif key == r3: 110 | subprocess.Popen("/home/odroid/clear_etch.sh", shell=False) 111 | sleep(.1) 112 | 113 | elif key == l0: 114 | cmd = 'echo "etch" > /home/odroid/startup' 115 | os.system(cmd) 116 | sleep(.1) 117 | elif key == r0: 118 | cmd = 'echo "gqrx" > /home/odroid/startup' 119 | os.system(cmd) 120 | sleep(.1) 121 | 122 | else: 123 | print 'fail' 124 | print key 125 | sleep(.01) # delay -------------------------------------------------------------------------------- /etch-a-node/jade/etch.jade: -------------------------------------------------------------------------------- 1 | doctype html(lang='en') 2 | head 3 | meta(charset='utf-8') 4 | title EtchASDR 5 | link(rel='stylesheet', type='text/css', href='/assets/css/normalize.css') 6 | link(rel='stylesheet', type='text/css', href='/assets/css/style.css') 7 | body#home 8 | #etch 9 | canvas.inside(width='1900px', height='1050px') 10 | script(src='/assets/js/jquery.js') 11 | script(src='/assets/js/jquery-ui.js') 12 | script(src='/socket.io/socket.io.js') 13 | script(src='/assets/js/node.js') 14 | script(src='/assets/js/script.js') -------------------------------------------------------------------------------- /etch-a-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etch-a-sdr", 3 | "version": "0.0.1", 4 | "description": "etch-a-sdr", 5 | "dependencies": { 6 | "body-parser": "^1.11.0", 7 | "express": "latest", 8 | "forever": "latest", 9 | "jade": "latest", 10 | "redis": "latest", 11 | "socket.io": "latest" 12 | }, 13 | "author": "devnulling" 14 | } 15 | -------------------------------------------------------------------------------- /etch-a-node/public/assets/css/normalize.css: -------------------------------------------------------------------------------- 1 | /* ============================================================================= 2 | HTML5 display definitions 3 | ========================================================================== */ 4 | 5 | /* 6 | * Corrects block display not defined in IE6/7/8/9 & FF3 7 | */ 8 | 9 | article, 10 | aside, 11 | details, 12 | figcaption, 13 | figure, 14 | footer, 15 | header, 16 | hgroup, 17 | nav, 18 | section { 19 | display: block; 20 | } 21 | 22 | /* 23 | * Corrects inline-block display not defined in IE6/7/8/9 & FF3 24 | */ 25 | 26 | audio, 27 | canvas, 28 | video { 29 | display: inline-block; 30 | *display: inline; 31 | *zoom: 1; 32 | } 33 | 34 | /* 35 | * Prevents modern browsers from displaying 'audio' without controls 36 | */ 37 | 38 | audio:not([controls]) { 39 | display: none; 40 | } 41 | 42 | /* 43 | * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 44 | * Known issue: no IE6 support 45 | */ 46 | 47 | [hidden] { 48 | display: none; 49 | } 50 | 51 | 52 | /* ============================================================================= 53 | Base 54 | ========================================================================== */ 55 | 56 | /* 57 | * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units 58 | * http://clagnut.com/blog/348/#c790 59 | * 2. Keeps page centred in all browsers regardless of content height 60 | * 3. Prevents iOS text size adjust after orientation change, without disabling user zoom 61 | * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ 62 | */ 63 | 64 | html { 65 | font-size: 100%; /* 1 */ 66 | overflow-y: scroll; /* 2 */ 67 | -webkit-text-size-adjust: 100%; /* 3 */ 68 | -ms-text-size-adjust: 100%; /* 3 */ 69 | } 70 | 71 | /* 72 | * Addresses margins handled incorrectly in IE6/7 73 | */ 74 | 75 | body { 76 | margin: 0; 77 | } 78 | 79 | /* 80 | * Addresses font-family inconsistency between 'textarea' and other form elements. 81 | */ 82 | 83 | body, 84 | button, 85 | input, 86 | select, 87 | textarea { 88 | font-family: sans-serif; 89 | } 90 | 91 | 92 | /* ============================================================================= 93 | Links 94 | ========================================================================== */ 95 | 96 | a { 97 | color: #00e; 98 | } 99 | 100 | a:visited { 101 | color: #551a8b; 102 | } 103 | 104 | /* 105 | * Addresses outline displayed oddly in Chrome 106 | */ 107 | 108 | a:focus { 109 | outline: thin dotted; 110 | } 111 | 112 | /* 113 | * Improves readability when focused and also mouse hovered in all browsers 114 | * people.opera.com/patrickl/experiments/keyboard/test 115 | */ 116 | 117 | a:hover, 118 | a:active { 119 | outline: 0; 120 | } 121 | 122 | 123 | /* ============================================================================= 124 | Typography 125 | ========================================================================== */ 126 | 127 | /* 128 | * Addresses styling not present in IE7/8/9, S5, Chrome 129 | */ 130 | 131 | abbr[title] { 132 | border-bottom: 1px dotted; 133 | } 134 | 135 | /* 136 | * Addresses style set to 'bolder' in FF3/4, S4/5, Chrome 137 | */ 138 | 139 | b, 140 | strong { 141 | font-weight: bold; 142 | } 143 | 144 | blockquote { 145 | margin: 1em 40px; 146 | } 147 | 148 | /* 149 | * Addresses styling not present in S5, Chrome 150 | */ 151 | 152 | dfn { 153 | font-style: italic; 154 | } 155 | 156 | /* 157 | * Addresses styling not present in IE6/7/8/9 158 | */ 159 | 160 | mark { 161 | background: #ff0; 162 | color: #000; 163 | } 164 | 165 | /* 166 | * Corrects font family set oddly in IE6, S4/5, Chrome 167 | * en.wikipedia.org/wiki/User:Davidgothberg/Test59 168 | */ 169 | 170 | pre, 171 | code, 172 | kbd, 173 | samp { 174 | font-family: monospace, serif; 175 | _font-family: 'courier new', monospace; 176 | font-size: 1em; 177 | } 178 | 179 | /* 180 | * Improves readability of pre-formatted text in all browsers 181 | */ 182 | 183 | pre { 184 | white-space: pre; 185 | white-space: pre-wrap; 186 | word-wrap: break-word; 187 | } 188 | 189 | /* 190 | * 1. Addresses CSS quotes not supported in IE6/7 191 | * 2. Addresses quote property not supported in S4 192 | */ 193 | 194 | /* 1 */ 195 | 196 | q { 197 | quotes: none; 198 | } 199 | 200 | /* 2 */ 201 | 202 | q:before, 203 | q:after { 204 | content: ''; 205 | content: none; 206 | } 207 | 208 | small { 209 | font-size: 75%; 210 | } 211 | 212 | /* 213 | * Prevents sub and sup affecting line-height in all browsers 214 | * gist.github.com/413930 215 | */ 216 | 217 | sub, 218 | sup { 219 | font-size: 75%; 220 | line-height: 0; 221 | position: relative; 222 | vertical-align: baseline; 223 | } 224 | 225 | sup { 226 | top: -0.5em; 227 | } 228 | 229 | sub { 230 | bottom: -0.25em; 231 | } 232 | 233 | 234 | /* ============================================================================= 235 | Lists 236 | ========================================================================== */ 237 | 238 | ul, 239 | ol { 240 | margin: 1em 0; 241 | padding: 0 0 0 40px; 242 | } 243 | 244 | dd { 245 | margin: 0 0 0 40px; 246 | } 247 | 248 | nav ul, 249 | nav ol { 250 | list-style: none; 251 | list-style-image: none; 252 | } 253 | 254 | 255 | /* ============================================================================= 256 | Embedded content 257 | ========================================================================== */ 258 | 259 | /* 260 | * 1. Removes border when inside 'a' element in IE6/7/8/9, F3 261 | * 2. Improves image quality when scaled in IE7 262 | * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ 263 | */ 264 | 265 | img { 266 | border: 0; /* 1 */ 267 | -ms-interpolation-mode: bicubic; /* 2 */ 268 | } 269 | 270 | /* 271 | * Corrects overflow displayed oddly in IE9 272 | */ 273 | 274 | svg:not(:root) { 275 | overflow: hidden; 276 | } 277 | 278 | 279 | /* ============================================================================= 280 | Figures 281 | ========================================================================== */ 282 | 283 | /* 284 | * Addresses margin not present in IE6/7/8/9, S5, O11 285 | */ 286 | 287 | figure { 288 | margin: 0; 289 | } 290 | 291 | 292 | /* ============================================================================= 293 | Forms 294 | ========================================================================== */ 295 | 296 | /* 297 | * Corrects margin displayed oddly in IE6/7 298 | */ 299 | 300 | form { 301 | margin: 0; 302 | } 303 | 304 | /* 305 | * Define consistent margin and padding 306 | */ 307 | 308 | fieldset { 309 | margin: 0 2px; 310 | padding: 0.35em 0.625em 0.75em; 311 | } 312 | 313 | /* 314 | * 1. Corrects color not being inherited in IE6/7/8/9 315 | * 2. Corrects alignment displayed oddly in IE6/7 316 | */ 317 | 318 | legend { 319 | border: 0; /* 1 */ 320 | *margin-left: -7px; /* 2 */ 321 | } 322 | 323 | /* 324 | * 1. Corrects font size not being inherited in all browsers 325 | * 2. Addresses margins set differently in IE6/7, F3/4, S5, Chrome 326 | * 3. Improves appearance and consistency in all browsers 327 | */ 328 | 329 | button, 330 | input, 331 | select, 332 | textarea { 333 | font-size: 100%; /* 1 */ 334 | margin: 0; /* 2 */ 335 | vertical-align: baseline; /* 3 */ 336 | *vertical-align: middle; /* 3 */ 337 | } 338 | 339 | /* 340 | * 1. Addresses FF3/4 setting line-height using !important in the UA stylesheet 341 | * 2. Corrects inner spacing displayed oddly in IE6/7 342 | */ 343 | 344 | button, 345 | input { 346 | line-height: normal; /* 1 */ 347 | *overflow: visible; /* 2 */ 348 | } 349 | 350 | /* 351 | * Corrects overlap and whitespace issue for buttons and inputs in IE6/7 352 | * Known issue: reintroduces inner spacing 353 | */ 354 | 355 | table button, 356 | table input { 357 | *overflow: auto; 358 | } 359 | 360 | /* 361 | * 1. Improves usability and consistency of cursor style between image-type 'input' and others 362 | * 2. Corrects inability to style clickable 'input' types in iOS 363 | */ 364 | 365 | button, 366 | html input[type="button"], 367 | input[type="reset"], 368 | input[type="submit"] { 369 | cursor: pointer; /* 1 */ 370 | -webkit-appearance: button; /* 2 */ 371 | } 372 | 373 | /* 374 | * 1. Addresses box sizing set to content-box in IE8/9 375 | * 2. Addresses excess padding in IE8/9 376 | */ 377 | 378 | input[type="checkbox"], 379 | input[type="radio"] { 380 | box-sizing: border-box; /* 1 */ 381 | padding: 0; /* 2 */ 382 | } 383 | 384 | /* 385 | * 1. Addresses appearance set to searchfield in S5, Chrome 386 | * 2. Addresses box sizing set to border-box in S5, Chrome (include -moz to future-proof) 387 | */ 388 | 389 | input[type="search"] { 390 | -webkit-appearance: textfield; /* 1 */ 391 | -moz-box-sizing: content-box; 392 | -webkit-box-sizing: content-box; /* 2 */ 393 | box-sizing: content-box; 394 | } 395 | 396 | /* 397 | * Corrects inner padding displayed oddly in S5, Chrome on OSX 398 | */ 399 | 400 | input[type="search"]::-webkit-search-decoration { 401 | -webkit-appearance: none; 402 | } 403 | 404 | /* 405 | * Corrects inner padding and border displayed oddly in FF3/4 406 | * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ 407 | */ 408 | 409 | button::-moz-focus-inner, 410 | input::-moz-focus-inner { 411 | border: 0; 412 | padding: 0; 413 | } 414 | 415 | /* 416 | * 1. Removes default vertical scrollbar in IE6/7/8/9 417 | * 2. Improves readability and alignment in all browsers 418 | */ 419 | 420 | textarea { 421 | overflow: auto; /* 1 */ 422 | vertical-align: top; /* 2 */ 423 | } 424 | 425 | 426 | /* ============================================================================= 427 | Tables 428 | ========================================================================== */ 429 | 430 | /* 431 | * Remove most spacing between table cells 432 | */ 433 | 434 | table { 435 | border-collapse: collapse; 436 | border-spacing: 0; 437 | } 438 | -------------------------------------------------------------------------------- /etch-a-node/public/assets/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color:#696969; 3 | background-image: -moz-radial-gradient(50% 50%, ellipse closest-side, #696969, #474747 70%); 4 | background-image: -webkit-radial-gradient(50% 50%, ellipse closest-side, #696969, #474747 70%); 5 | background-image: -o-radial-gradient(50% 50%, ellipse closest-side, #696969, #474747 70%); 6 | background-image: -ms-radial-gradient(50% 50%, ellipse closest-side, #696969, #474747 70%); 7 | background-image: radial-gradient(50% 50%, ellipse closest-side, #696969, #474747 70%) 8 | } 9 | 10 | #etch { 11 | height: 1050px; 12 | width: 1900px; 13 | position: relative; 14 | cursor: move; 15 | } 16 | 17 | #etch .inside { 18 | background-color: #EEEEEE; 19 | border-radius: 0.3em; 20 | box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.2), 0 0 8px #555555 inset; 21 | height: 1050px; 22 | width: 1900px; 23 | margin-top: 1em; 24 | } 25 | 26 | #etch h1 { 27 | color: #F2F2BF; 28 | font-size: 40px; 29 | margin: 0; 30 | text-shadow: 1px 1px 2px #000000; 31 | text-align: center; 32 | } 33 | 34 | #etch h1 span { 35 | display: inline-block; 36 | font-family: verdana; 37 | font-size: 18px; 38 | font-style: italic; 39 | padding: 0 10px; 40 | position: relative; 41 | text-transform: uppercase; 42 | top: -4px; 43 | } 44 | 45 | #etch span.knob { 46 | background-color: #fff; 47 | } 48 | 49 | .knob { 50 | -moz-transition: all 0.3s linear 0s; 51 | background-color: #FAFAFA; 52 | background-image: -moz-radial-gradient(70% 70% , ellipse closest-side, #FAFAFA, #D9D9D9 10%, #F2F2F2 45%, #DDDDDD 170%); 53 | border-radius: 60px 60px 60px 60px; 54 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5), 0 0 3px #666666 inset; 55 | display: block; 56 | height: 60px; 57 | width: 60px; 58 | bottom: 13px; 59 | position: absolute; 60 | } 61 | 62 | .knob_r { 63 | right: 60px; 64 | } 65 | .knob_l { 66 | left: 60px; 67 | } 68 | .about { 69 | color: #D40019; 70 | display: block; 71 | font-size: 1.3em; 72 | text-align: center; 73 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6), 74 | -1px -1px 1px rgba(255, 255, 255, 0.5); 75 | } 76 | .about a { 77 | color: #D40019; 78 | font-weight: bold; 79 | text-decoration: none; 80 | -moz-transition: all 0.3s linear; 81 | -webkit-transition: all 0.3s linear; 82 | } 83 | .about a:hover { 84 | color: #B20006; 85 | } 86 | 87 | footer { 88 | color: #CCCCCC; 89 | text-align: center; 90 | text-shadow: 2px 2px 1px #000000; 91 | line-height: 1.5em; 92 | } 93 | 94 | footer div { 95 | color: #888888; 96 | padding: 5px 0 3em; 97 | text-shadow: 0 0; 98 | } 99 | 100 | footer p { 101 | margin: 0; 102 | padding: 0; 103 | } 104 | 105 | footer p a, 106 | footer p a:visited { 107 | color: #AAA; 108 | text-decoration: none; 109 | } 110 | footer p a:hover { 111 | color: #999; 112 | } 113 | 114 | .icons { 115 | bottom: 28px; 116 | position: relative; 117 | right: 12px; 118 | text-align: right; 119 | } 120 | 121 | .icons a { 122 | opacity: 0.8; 123 | text-decoration: none; 124 | cursor: pointer; 125 | } 126 | .icons a:hover { 127 | opacity: 1; 128 | } 129 | 130 | -------------------------------------------------------------------------------- /etch-a-node/public/assets/js/node.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | var socket = io.connect('http://localhost:7200'); 3 | socket.on('message', function (data) { 4 | if (data.message) { 5 | parent.processSocket(data.message); 6 | console.log(JSON.stringify(data)); 7 | } else { 8 | console.log("There is a problem:", data); 9 | } 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /etch-a-node/public/assets/js/script.js: -------------------------------------------------------------------------------- 1 | /* Local vars */ 2 | var etch = $('#etch canvas')[0], 3 | context = etch.getContext('2d'), 4 | /* Center at first */ 5 | cur_x = 850, cur_y = 525, 6 | /* Rotation for knobs */ 7 | rot_x = rot_y = 0, 8 | /* The path object (and current index) */ 9 | path = {0: [cur_x, cur_y]}, i = 0, 10 | /* Current direction the user is drawing */ 11 | current_direction = false, 12 | /* Does this have localStorage? */ 13 | has_local_storage = supports_local_storage(), 14 | /* Device orientation variables */ 15 | old_arguments = false, 16 | /* How sensitive should the shaking be? */ 17 | sensitivity = 30; 18 | 19 | 20 | /* Set up the line color and width */ 21 | context.strokeStyle = "rgba(0,0,0,0.75)"; 22 | //context.strokeStyle = "rgba(90,90,90,0.75)"; 23 | context.lineWidth = 1.5; 24 | 25 | 26 | var red = {r:255,g:0,b:0}; 27 | var blue = {r:0,g:0,b:255}; 28 | var green = {r:0,g:255,b:0}; 29 | var black = {r:0, g:0, b:0}; 30 | var colors = new Array(black, blue, green, red); 31 | var clen = colors.length; 32 | 33 | //console.log(colors[0].r); 34 | var curcol = 0; 35 | var selcol = colors[curcol]; 36 | 37 | function setWhite(){ 38 | context.strokeStyle = "rgba(238,238,238,0.75)"; 39 | } 40 | function setBlack(){ 41 | context.strokeStyle = "rgba(0,0,0,0.75)"; 42 | } 43 | 44 | function incColor(){ 45 | curcol++; 46 | if(curcol > (clen-1)){ 47 | curcol = 0; 48 | } 49 | selcol = colors[curcol]; 50 | setColor(); 51 | } 52 | 53 | function decColor(){ 54 | curcol--; 55 | if(curcol < 0){ 56 | curcol = clen - 1; 57 | } 58 | selcol = colors[curcol]; 59 | setColor(); 60 | } 61 | 62 | function setColor(){ 63 | console.log("setting new color"); 64 | console.log("r: " + secol.r); 65 | console.log("g: " + secol.g); 66 | console.log("b: " + secol.b); 67 | 68 | context.strokeStyle = "rgba(selcol.r,selcol.g,selcol.b,0.75)"; 69 | } 70 | 71 | /* Get a URL for the user. */ 72 | function getURL(hash) { 73 | hash = hash || getHash(); 74 | return window.location.href.split('#')[0] + '#' + hash; 75 | } 76 | 77 | /* Get the hash from the path */ 78 | function getHash() { 79 | return Base64.encode(JSON.stringify(path)) 80 | } 81 | 82 | function processSocket(msg) { 83 | console.log("new socket msg: " + msg); 84 | if (msg == "r") { 85 | drawLineSocket(1, 0, 0); 86 | } else if (msg == "l") { 87 | drawLineSocket(-1, 0, 0); 88 | } else if (msg == "d") { 89 | drawLineSocket(0, -1, 0); 90 | } else if (msg == "u") { 91 | drawLineSocket(0, 1, 0); 92 | } else if (msg == "R") { 93 | drawLineSocket(1, 0, 1); 94 | } else if (msg == "L") { 95 | drawLineSocket(-1, 0, 1); 96 | } else if (msg == "U") { 97 | drawLineSocket(0, -1, 1); 98 | } else if (msg == "D") { 99 | drawLineSocket(0, 1, 1); 100 | } else if (msg == "a") { 101 | incColor(); 102 | console.log("inc color"); 103 | } else if (msg == "b") { 104 | decColor(); 105 | console.log("dec color"); 106 | } else if (msg == "c") { 107 | // clear 108 | clearEtch(); 109 | console.log("Clearing etch"); 110 | } else { 111 | return; // Not an arrow key; carry on! 112 | } 113 | } 114 | 115 | 116 | function drawLineSocket(x, y, shift) { 117 | 118 | // Set up the path stuff 119 | context.beginPath(); 120 | context.moveTo(cur_x, cur_y); 121 | 122 | // Set the new new location. 123 | // var shift = false; 124 | cur_x += 3 * x * (shift ? 4 : 1); 125 | cur_y += 3 * y * (shift ? 4 : 1); 126 | 127 | // Set the rotation of the knobs. 128 | rot_x += 1 * x; 129 | rot_y += 1 * y; 130 | 131 | // Limit! 132 | if (cur_x <= 0) { 133 | cur_x = 0; 134 | } else if (cur_y <= 0) { 135 | cur_y = 0; 136 | } else if (cur_x >= 1900) { 137 | cur_x = 1900; 138 | } else if (cur_y >= 1050) { 139 | cur_y = 1050; 140 | } 141 | 142 | // Set the direction the user is currently drawing. 143 | var new_direction = x + "--" + y; 144 | if (current_direction != new_direction) { 145 | // New direction, so we need to add a new line. 146 | i++; 147 | current_direction = new_direction; 148 | } 149 | // Update or add a new line. 150 | path[i] = [cur_x, cur_y]; 151 | 152 | // Rotate the knob (Fx only so far). 153 | $('.knob_l').css('-moz-transform', 'rotate(' + rot_x * 50 + 'deg)'); 154 | $('.knob_r').css('-moz-transform', 'rotate(' + rot_y * 50 + 'deg)'); 155 | 156 | // Draw the line and stroke it. 157 | context.lineTo(cur_x, cur_y); 158 | context.closePath(); 159 | context.stroke(); 160 | 161 | /* TODO: Make this async! */ 162 | if (has_local_storage) { 163 | window.localStorage['path'] = getHash(); 164 | } 165 | 166 | // Reset hash 167 | window.location.hash = ''; 168 | } 169 | 170 | 171 | 172 | /* The user just pressed an arrow key. */ 173 | function drawLine(x, y, e) { 174 | // Set up the path stuff 175 | context.beginPath(); 176 | context.moveTo(cur_x, cur_y); 177 | 178 | // Set the new new location. 179 | var shift = e.shiftKey; 180 | cur_x += 3 * x * (shift ? 4 : 1); 181 | cur_y += 3 * y * (shift ? 4 : 1); 182 | 183 | // Set the rotation of the knobs. 184 | rot_x += 1 * x; 185 | rot_y += 1 * y; 186 | 187 | // Limit! 188 | if (cur_x <= 0) { 189 | cur_x = 0; 190 | } else if (cur_y <= 0) { 191 | cur_y = 0; 192 | } else if (cur_x >= 1900) { 193 | cur_x = 1900; 194 | } else if (cur_y >= 1050) { 195 | cur_y = 1050; 196 | } 197 | 198 | // Set the direction the user is currently drawing. 199 | var new_direction = x + "--" + y; 200 | if (current_direction != new_direction) { 201 | // New direction, so we need to add a new line. 202 | i++; 203 | current_direction = new_direction; 204 | } 205 | // Update or add a new line. 206 | path[i] = [cur_x, cur_y]; 207 | 208 | // Rotate the knob (Fx only so far). 209 | $('.knob_l').css('-moz-transform', 'rotate(' + rot_x * 50 + 'deg)'); 210 | $('.knob_r').css('-moz-transform', 'rotate(' + rot_y * 50 + 'deg)'); 211 | 212 | // Draw the line and stroke it. 213 | context.lineTo(cur_x, cur_y); 214 | context.closePath(); 215 | context.stroke(); 216 | 217 | /* TODO: Make this async! */ 218 | if (has_local_storage) { 219 | window.localStorage['path'] = getHash(); 220 | } 221 | 222 | // Reset hash 223 | window.location.hash = ''; 224 | } 225 | 226 | /* We're ready to go! */ 227 | $(function () { 228 | $(document).keydown(function (e) { 229 | if (e.keyCode == jQuery.ui.keyCode.RIGHT) { 230 | drawLine(1, 0, e); 231 | } else if (e.keyCode == jQuery.ui.keyCode.LEFT) { 232 | drawLine(-1, 0, e); 233 | } else if (e.keyCode == jQuery.ui.keyCode.UP) { 234 | drawLine(0, -1, e); 235 | } else if (e.keyCode == jQuery.ui.keyCode.DOWN) { 236 | drawLine(0, 1, e); 237 | } else { 238 | return; // Not an arrow key; carry on! 239 | } 240 | // It is an arrow; so don't move the page. 241 | e.preventDefault(); 242 | }); 243 | 244 | 245 | /* Do we have a saved one? */ 246 | var existing = false; 247 | 248 | // Check the hash 249 | if (location.hash && location.hash.length > 5) { 250 | existing = location.hash.replace(/#/, '') 251 | if (existing.match(/^blob/)) { 252 | $.get('saved/' + existing + '.json', load_existing); 253 | } else { 254 | load_existing(window.localStorage['path']); 255 | } 256 | } 257 | // Check localStorage 258 | else if (has_local_storage) { 259 | load_existing(window.localStorage['path']); 260 | } 261 | 262 | /* Device Orientation Shake */ 263 | // Somewhat based on code by Jeffery Harrell and HTML5Rocks. 264 | if (window.DeviceMotionEvent) { 265 | $(window).bind("devicemotion", function (e) { 266 | var oe = e.originalEvent; 267 | movement(oe.accelerationIncludingGravity.x * 30, 268 | oe.accelerationIncludingGravity.y * 30, 269 | oe.accelerationIncludingGravity.z * 30) 270 | }); 271 | } else if (window.DeviceOrientationEvent) { 272 | $(window).bind('deviceorientation', function (e) { 273 | var oe = e.originalEvent; 274 | movement(oe.gamma, oe.beta); 275 | }); 276 | } else if (window.OrientationEvent) { 277 | $(window).bind('MozOrientation', function (e) { 278 | var oe = e.originalEvent; 279 | movement(oe.x * 90, oe.y * -90); 280 | }); 281 | } else { 282 | $('#orientation_supported').remove(); 283 | } 284 | 285 | }); 286 | 287 | /* Shake and bake (DOM) */ 288 | var pos_x = pos_y = false, 289 | direction_x = direction_y = 0, 290 | total_x = total_y = 0, 291 | timer = false; 292 | 293 | $("#etch").draggable({revert: true, scroll: false, 294 | start: function () { 295 | /* Every 300 miliseconds it's being dragged, take away a shake. 296 | * This is so we can move it around slowly without it being cleared. */ 297 | timer = setInterval(function () { 298 | total_x--; 299 | total_y--; 300 | if (total_x < 0) 301 | total_x = 0; 302 | if (total_y < 0) 303 | total_y = 0; 304 | }, 300); 305 | }, 306 | drag: function (e) { 307 | var new_direction_x = e.pageX - pos_x > 0 ? 1 : -1, 308 | new_direction_y = e.pageY - pos_y > 0 ? 1 : -1; 309 | 310 | // If it's moved 10+ pixels and it's moved in another direction... 311 | if (Math.abs(e.pageX - pos_x) > 10 && new_direction_x != direction_x) { 312 | direction_x = new_direction_x; 313 | total_x++; 314 | } 315 | if (Math.abs(e.pageY - pos_y) > 10 && new_direction_y != direction_y) { 316 | direction_y = new_direction_y; 317 | total_y++; 318 | } 319 | 320 | // Set the "old" position of the div. 321 | pos_x = e.pageX; 322 | pos_y = e.pageY; 323 | 324 | // If the modal has changed directions 4+ times, reset. 325 | if (total_x >= 4 || total_y >= 4) { 326 | clearEtch(); 327 | } 328 | }, 329 | stop: function () { 330 | // Collaborate and listen. 331 | pos_x = pos_y = false; 332 | direction_x = direction_y = 0; 333 | total_x = total_y = 0; 334 | clearInterval(timer); 335 | } 336 | }); 337 | 338 | function movement() { 339 | var change = 0; 340 | if (old_arguments) { 341 | for (var l = 0; l < arguments.length; l++) { 342 | change += arguments[l]; 343 | change -= old_arguments[l]; 344 | } 345 | 346 | if (Math.abs(change) > 10 * arguments.length) { 347 | clearEtch(); 348 | } 349 | } 350 | old_arguments = arguments; 351 | } 352 | 353 | function resetCursor() { 354 | 355 | // Set up the path stuff 356 | context.beginPath(); 357 | context.moveTo(cur_x, cur_y); 358 | 359 | cur_x = 1900/2; 360 | cur_y = 1050/2; 361 | 362 | // Draw the line and stroke it. 363 | context.lineTo(cur_x, cur_y); 364 | context.closePath(); 365 | context.stroke(); 366 | 367 | // Reset hash 368 | window.location.hash = ''; 369 | } 370 | 371 | 372 | function clearEtch() { 373 | setWhite(); 374 | 375 | if (i == 0) 376 | return; // Already clear. 377 | location.hash = ''; 378 | context.clearRect(0, 0, 1900, 1900); 379 | path = {0: [cur_x, cur_y]}; 380 | i = 0; 381 | current_direction = false; 382 | if (has_local_storage) { 383 | window.localStorage['path'] = path; 384 | } 385 | resetCursor(); 386 | setBlack(); 387 | } 388 | 389 | // Does it support local storage? 390 | function supports_local_storage() { 391 | try { 392 | return 'localStorage' in window && window['localStorage'] !== null; 393 | } catch (e) { 394 | return false; 395 | } 396 | } 397 | 398 | function load_existing(existing) { 399 | // We found something! 400 | if (existing) { 401 | var go_x, go_y; 402 | 403 | if (typeof existing != 'object') { 404 | try { 405 | existing = JSON.parse(Base64.decode(existing)); 406 | } catch (e) { 407 | return; // We couldn't parse it. Ah well, we tried. 408 | } 409 | } 410 | 411 | // Draw all the lines. TODO: combine this with above. 412 | var highest_i; 413 | $.each(existing, function (k, v) { 414 | context.beginPath(); 415 | context.moveTo(go_x, go_y); 416 | 417 | context.lineTo(v[0], v[1]); 418 | context.closePath(); 419 | context.stroke(); 420 | 421 | go_x = v[0]; 422 | go_y = v[1]; 423 | highest_i = k; 424 | }); 425 | 426 | // Set the starting points. 427 | path = existing; 428 | cur_x = ~~go_x; 429 | cur_y = ~~go_y; 430 | i = parseInt(highest_i); 431 | } 432 | } 433 | 434 | -------------------------------------------------------------------------------- /etch_controls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | file="/home/odroid/startup" 3 | mode=$(cat $file) 4 | 5 | if [ $mode == "etch" ] 6 | then 7 | /usr/bin/screen -d -m /usr/bin/python /home/odroid/etch-a-node/controls.py 8 | fi 9 | 10 | -------------------------------------------------------------------------------- /etch_start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pymouse import PyMouse 4 | from pykeyboard import PyKeyboard 5 | 6 | from time import sleep 7 | 8 | m = PyMouse() 9 | k = PyKeyboard() 10 | 11 | xd, yd = m.screen_size() 12 | 13 | k.tap_key(k.function_keys[11]) 14 | 15 | print xd 16 | print yd 17 | sleep(1) 18 | m.click(1500,1080,1) 19 | -------------------------------------------------------------------------------- /gqrx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /usr/local/bin/gqrx & 3 | sleep 5 4 | 5 | -------------------------------------------------------------------------------- /gqrx_arduino.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import telnetlib 3 | import subprocess 4 | from time import sleep 5 | import os 6 | 7 | debugMode = 0 8 | currentMode = "OFF" 9 | stepLeft = 1000000 10 | stepRight = 10000 11 | 12 | freqSteps = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000] 13 | deModes = ['OFF', 'RAW', 'AM', 'FM', 'WFM', 'WFM_ST', 'LSB', 'USB', 'CW', 'CWL', 'CWU'] 14 | 15 | class Gqrx(): 16 | 17 | def _request(self, request): 18 | global debugMode 19 | if debugMode == 1: 20 | print request 21 | else: 22 | con = telnetlib.Telnet('127.0.0.1', 7356) 23 | con.write(('%s\n' % request).encode('ascii')) 24 | response = con.read_some().decode('ascii').strip() 25 | con.write('c\n'.encode('ascii')) 26 | return response 27 | 28 | def set_frequency(self, frequency): 29 | return self._request('F %s' % frequency) 30 | 31 | def get_frequency(self): 32 | return self._request('f') 33 | 34 | def set_mode(self, mode): 35 | return self._request('M %s' % mode) 36 | 37 | def get_mode(self): 38 | return self._request('m') 39 | 40 | def get_level(self): 41 | return self._request('l') 42 | 43 | 44 | def getlevel(): 45 | gqrx = Gqrx() 46 | level = gqrx.get_level() 47 | print level 48 | 49 | def switchmode(): 50 | global currentMode 51 | global deModes 52 | 53 | curModePos = deModes.index(currentMode) 54 | newModePos = int(curModePos) + 1 55 | 56 | if newModePos >= len(deModes): 57 | newModePos = 0 58 | 59 | currentMode = deModes[newModePos] 60 | gqrx = Gqrx() 61 | result = gqrx.set_mode(currentMode) 62 | print result 63 | print currentMode 64 | 65 | 66 | 67 | def incstep(side): 68 | global stepLeft 69 | global stepRight 70 | global freqSteps 71 | 72 | if side == "left": 73 | currentStep = stepLeft 74 | stepPos = freqSteps.index(currentStep) 75 | newPos = int(stepPos) + 1 76 | if newPos >= len(freqSteps): 77 | newPos = 0 78 | stepLeft = freqSteps[newPos] 79 | elif side == "right": 80 | currentStep = stepRight 81 | stepPos = freqSteps.index(currentStep) 82 | newPos = int(stepPos) + 1 83 | if newPos >= len(freqSteps): 84 | newPos = 0 85 | stepRight = freqSteps[newPos] 86 | 87 | 88 | def decstep(side): 89 | global stepLeft 90 | global stepRight 91 | global freqSteps 92 | 93 | if side == "left": 94 | currentStep = stepLeft 95 | stepPos = freqSteps.index(currentStep) 96 | newPos = int(stepPos) - 1 97 | if newPos == -1: 98 | newPos = len(freqSteps)-1 99 | stepLeft = freqSteps[newPos] 100 | elif side == "right": 101 | currentStep = stepRight 102 | stepPos = freqSteps.index(currentStep) 103 | newPos = int(stepPos) - 1 104 | if newPos == -1: 105 | newPos = len(freqSteps)-1 106 | stepRight = freqSteps[newPos] 107 | 108 | 109 | def incfreq(side): 110 | global stepLeft 111 | global stepRight 112 | currentStep = 0 113 | left = "left" 114 | right = "right" 115 | 116 | if side == left: 117 | currentStep = stepLeft 118 | elif side == right: 119 | currentStep = stepRight 120 | else: 121 | currentStep = 0 122 | 123 | print 'running' 124 | if debugMode == 1: 125 | freq = 1 126 | else: 127 | gqrx = Gqrx() 128 | freq = gqrx.get_frequency() 129 | print freq 130 | setfreq = int(freq) + currentStep 131 | set = gqrx.set_frequency(setfreq) 132 | nf = gqrx.get_frequency() 133 | print nf 134 | 135 | def decfreq(side): 136 | global stepLeft 137 | global stepRight 138 | currentStep = 0 139 | left = "left" 140 | right = "right" 141 | 142 | if side == left: 143 | currentStep = stepLeft 144 | elif side == right: 145 | currentStep = stepRight 146 | else: 147 | currentStep = 0 148 | 149 | print 'running dec freq' 150 | if debugMode == 1: 151 | freq = 1 152 | else: 153 | gqrx = Gqrx() 154 | freq = gqrx.get_frequency() 155 | print freq 156 | setfreq = int(freq)-currentStep 157 | set = gqrx.set_frequency(setfreq) 158 | nf = gqrx.get_frequency() 159 | print nf 160 | 161 | ser = serial.Serial('/dev/ttyACM0', 115200) 162 | 163 | r0 = "R0" 164 | l0 = "L0" 165 | 166 | r1 = "R1" 167 | r2 = "R2" 168 | r3 = "R3" 169 | 170 | l1 = "L1" 171 | l2 = "L2" 172 | l3 = "L3" 173 | 174 | l_up = "LU" 175 | l_down = "LD" 176 | 177 | r_up = "RU" 178 | r_down = "RD" 179 | 180 | mp = "MP" 181 | 182 | while True: 183 | newline = ser.readline() 184 | newline = newline.rstrip() 185 | 186 | key = newline[:2] 187 | print key 188 | 189 | if key == mp: 190 | print "MPU" 191 | 192 | elif key == r1: 193 | print 'r1 button' 194 | print stepRight 195 | print 'increasing stepRight' 196 | incstep("right") 197 | print 'increased stepRight' 198 | print stepRight 199 | 200 | elif key == r2: 201 | print 'r2 button' 202 | print stepRight 203 | print 'decreasing stepRight' 204 | decstep("right") 205 | print 'decreased stepRight' 206 | print stepRight 207 | 208 | elif key == r3: 209 | if debugMode == 1: 210 | subprocess.Popen("/home/odroid/start.py", shell=False) 211 | sleep(1) 212 | debugMode = 0 213 | sleep(1) 214 | else: 215 | print 'r3 button' 216 | print currentMode 217 | print 'changing mode' 218 | switchmode() 219 | print 'done' 220 | print currentMode 221 | 222 | elif key == l3: 223 | subprocess.Popen("/home/odroid/reboot.sh", shell=False) 224 | sleep(1) 225 | 226 | elif key == l1: 227 | print 'l1 button' 228 | print stepLeft 229 | print 'increasing stepLeft' 230 | incstep("left") 231 | print 'increased stepLeft' 232 | print stepLeft 233 | 234 | elif key == l2: 235 | print 'l2 button' 236 | print stepLeft 237 | print 'decreasing stepLeft' 238 | decstep("left") 239 | print 'decreased stepLeft' 240 | print stepLeft 241 | 242 | 243 | elif key == l_up: 244 | print 'left up' 245 | knobval = newline[2:] 246 | print knobval 247 | print 'increasing freq' 248 | incfreq("left") 249 | print 'inc done' 250 | 251 | elif key == l_down: 252 | print 'left down' 253 | knobval = newline[2:] 254 | print knobval 255 | print 'decreasing freq' 256 | decfreq("left") 257 | print 'dec done' 258 | 259 | elif key == r_up: 260 | print 'right up' 261 | knobval = newline[2:] 262 | print knobval 263 | print 'increasing freq' 264 | incfreq("right") 265 | print 'inc done' 266 | 267 | elif key == r_down: 268 | print 'right down' 269 | knobval = newline[2:] 270 | print knobval 271 | print 'decreasing freq' 272 | decfreq("right") 273 | print 'dec done' 274 | elif key == l0: 275 | cmd = 'echo "etch" > /home/odroid/startup' 276 | os.system(cmd) 277 | sleep(1) 278 | elif key == r0: 279 | cmd = 'echo "gqrx" > /home/odroid/startup' 280 | os.system(cmd) 281 | sleep(1) 282 | 283 | else: 284 | print 'fail' 285 | print key 286 | sleep(.01) #delay -------------------------------------------------------------------------------- /gqrx_mouse.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | file="/home/odroid/startup" 3 | mode=$(cat $file) 4 | if [ $mode == "gqrx" ] 5 | then 6 | sleep 5 7 | DISPLAY=:0; /usr/bin/python /home/odroid/gqrx_start.py 8 | fi 9 | 10 | -------------------------------------------------------------------------------- /gqrx_start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pymouse import PyMouse 4 | from time import sleep 5 | 6 | sleep(10) 7 | 8 | m = PyMouse() 9 | 10 | xd, yd = m.screen_size() 11 | 12 | print xd 13 | print yd 14 | sleep(10) 15 | m.click(5,5,1) 16 | sleep(1) 17 | m.click(30, 75, 1) 18 | sleep(1) 19 | m.click(285, 75, 1) 20 | sleep(1) 21 | m.click(375, 75, 1) 22 | -------------------------------------------------------------------------------- /images/etch-a-sdr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnulling/etch-a-sdr/aea749c856d00250baa73cba4ca4d4c6e1955798/images/etch-a-sdr.jpg -------------------------------------------------------------------------------- /images/left_side.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnulling/etch-a-sdr/aea749c856d00250baa73cba4ca4d4c6e1955798/images/left_side.jpg -------------------------------------------------------------------------------- /images/right_side.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devnulling/etch-a-sdr/aea749c856d00250baa73cba4ca4d4c6e1955798/images/right_side.jpg -------------------------------------------------------------------------------- /reboot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | num=`cut -d ',' -f2 /home/odroid/rebootct.txt` 3 | 4 | if [ $num -gt 4 ] 5 | then 6 | echo "0" > /home/odroid/rebootct.txt 7 | sudo poweroff 8 | else 9 | oldnum=`cut -d ',' -f2 /home/odroid/rebootct.txt` 10 | newnum=`expr $oldnum + 1` 11 | sed -i "s/$oldnum\$/$newnum/g" /home/odroid/rebootct.txt 12 | fi 13 | -------------------------------------------------------------------------------- /rebootct.txt: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | file="/home/odroid/startup" 3 | mode=$(cat $file) 4 | 5 | echo "0" > /home/odroid/rebootct.txt 6 | sed -i 's/crashed\=true/crashed\=false/' /home/odroid/.config/gqrx/default.conf 7 | 8 | if [ $mode == "etch" ] 9 | then 10 | echo 'running etch start up' 11 | /bin/bash /home/odroid/start_etch.sh 12 | fi 13 | 14 | if [ $mode == "gqrx" ] 15 | then 16 | echo 'running gqrx start up' 17 | /usr/local/bin/gqrx & 18 | /bin/bash /home/odroid/controls.sh 19 | 20 | fi 21 | 22 | 23 | -------------------------------------------------------------------------------- /start_chrome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "starting chrome" 3 | /bin/bash /home/odroid/chrome.sh & 4 | echo "Sleeping for 15 seconds" 5 | sleep 15 6 | /usr/bin/python /home/odroid/etch_start.py & 7 | echo "Running fullscreen and mouse clear" 8 | -------------------------------------------------------------------------------- /start_etch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | screen -d -m /usr/local/bin/redis-server 4 | printf "Redis Started"; 5 | printf ""; 6 | sleep 1; 7 | 8 | screen -d -m /usr/local/bin/forever start /home/odroid/etch-a-node/app.js 9 | printf "nodejs app started"; 10 | printf ""; 11 | sleep 1; 12 | 13 | sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' /home/odroid/.config/chromium/Default/Preferences 14 | /bin/bash /home/odroid/etch_controls.sh & 15 | 16 | /bin/bash /home/odroid/start_chrome.sh & 17 | 18 | 19 | -------------------------------------------------------------------------------- /startup: -------------------------------------------------------------------------------- 1 | gqrx --------------------------------------------------------------------------------