├── .gitattributes ├── README.md ├── README.md.backup ├── ats100 ├── README.md ├── amapp.js ├── bardisp.js ├── button.js ├── chooser.js ├── fmapp.js ├── freqdisp.js ├── freqdispold.js ├── main.js ├── rds.js ├── selector.js ├── si4735.js ├── smallradio.png ├── ssbapp.js ├── st7789v.js └── stepdisp.js ├── ats25 ├── README.md ├── amradio.js ├── app_buttons.js ├── bardisp.js ├── button.js ├── chooser.js ├── fmradio.js ├── ili9342.js ├── keyboard.js ├── main.js ├── radio.png ├── rds.js ├── selector.js ├── si4735.js ├── ssbradio.js ├── ssbradioAGCnotworking.js ├── testapp.js └── xpt2046.js ├── bands.json ├── boot0.js ├── bootcde.js ├── encoder.js ├── espruino_2v13.11_esp32 ├── README_flash.txt ├── bootloader.bin ├── espruino_esp32.bin └── partitions_espruino.bin └── image ├── amapp.jpg ├── amradio.jpg ├── ats100.jpg ├── ats100home.jpg ├── ats25.jpg ├── dclick.jpg ├── fmapp.jpg ├── fmradio.jpg ├── home_screen.jpg ├── keys.jpg ├── ssbapp.jpg └── ssbradio.jpg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SI4732 Radios programmed with Espruino 2 | 3 | These all-band (FM,LW,MW,SW with SSB) radios (see pictures below) are available on AliExpress and Ebay. They consist of an SI4732-A10 chip which provides the radio functions driven via I2C by an ESP32 WROOM module which implements the display interface. In the case of the ATS25, this is an ILI9341 (with XPT2046 touch ) 320x240 pixel display. In the case of the ATS100, the display is an 240x135 ST7789V with no touch interface. Both radios have a rotary controller incoporation a push button switch. 4 | 5 | ![](image/ats25.jpg) ![](image/ats100.jpg) 6 | 7 | There is an extensive well documented Arduino library for the SI4732 and a sketch which implements the firmware running on the ATS25. The firmware can easily be updated from Arduino via the USB-C ports on the radios. However, the combined library and sketch is over 10,000 lines of C++ which makes it cumbersome to change so the Espruino versions are to faciltate experimentation. The Espruino implementation is not as responsive as the Arduino software, however, it permits rapid change using the [Espruino IDE](https://www.espruino.com/ide/). 8 | 9 | ![ATS25](ats25/README.md) 10 | 11 | 12 | ![ATS100](ats100/README.md) 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md.backup: -------------------------------------------------------------------------------- 1 | # SI4732 Radios programmed with Espruino 2 | 3 | These all-band (FM,LW,MW,SW with SSB) radios (see pictures below) are available on AliExpress and Ebay. They consist of an SI4732-A10 chip which provides the radio functions driven via I2C by an ESP32 WROOM module which implements the display interface. In the case of the ATS25, this is an ILI9341 (with XPT2046 touch ) 320x240 pixel display. In the case of the ATS100, the display is an 240x135 ST7789V with no touch interface. Both radios have a rotary controller incoporation a push button switch. 4 | 5 | ![](image/ats25.jpg) ![](image/ats100.jpg) 6 | 7 | There is an extensive well documented Arduino library for the SI4732 and a sketch which implements the firmware running on the ATS25. The firmware can easily be updated from Arduino via the USB-C ports on the radios. However, the combined library and sketch is over 10,000 lines of C++ which makes it cumbersome to change so the Espruino versions are to faciltate experimentation. The Espruino implementation is not as responsive as the Arduino software, however, it permits rapid change using the [Espruino IDE](https://www.espruino.com/ide/). 8 | 9 | ![ATS25](ats25/README.md) 10 | 11 | 12 | ![ATS100](ats100/README.md) 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ats100/README.md: -------------------------------------------------------------------------------- 1 | # ATS100 Radio programmed with Espruino 2 | 3 | The SI4732 essentially supports three separate radio modes, FM, AM (including SW) and SSB (via a patch). The set of commands and properties supported by these three modes is not the same although there is overlap - for example SSB does not have a scan command. Consequently, to facilitate experimentation and to simplify interfaces, each of the three modes is supported by a different radio app invoked from the home screen shown below: 4 | 5 | ![](../image/ats100home.jpg) 6 | 7 | In contrast to the ATS-25 which has a touch screen, all interaction is via the rotary controller. To select a button or a control such as the BAND selector, turn the knob until the button/control is highlighted, then push the button to activate. 8 | 9 | Each of the radio apps saves frequency, volume, brightness and bandwidth settings on exit and these are restored the next time the app is entered. 10 | 11 | ## FM Radio 12 | 13 | The FM radio application supports RDS and stations can be saved as presets in the list on the right of the screen shown below using the RDS information. 14 | 15 | ![](../image/fmapp.jpg) 16 | 17 | 18 | ## AM Radio 19 | 20 | The AM wavebands shown in the window on the right of the screen can be scolled through using the rotary controller. They are defined in a file stored in Espruino storage. This file ```bands.json``` can be easily modified using the Espruino IDE. 21 | 22 | ![](../image/amapp.jpg) 23 | 24 | ## SSB Radio 25 | 26 | There is a short delay - caused by the need to load the SSB patch into the SI4732 - before the SSB radio app launches. The wavebands are stored in the same ```bands.json``` file as for the AM radio. 27 | 28 | ![](../image/ssbapp.jpg) 29 | 30 | The tuning increment can be changed to 10,000Hz, 1000Hz, 100Hz or 10Hz using the rotary controller button - see 1000Hz underline in screen shot. Tuning uses the SSB patch BFO property to minimise 'chuff' on tuning to a different frequency using the tune command. 31 | 32 | ## Direct Frequency Input 33 | 34 | Each of the above three radio apps permits a user to directly input the desired frequency to tune by double clicking with the pushbutton on the frequency display. 35 | 36 | ![](../image/dclick.jpg) 37 | 38 | ## Installation instructions 39 | 40 | *WARNING: This will wipe the existing firmware on the ATS100 and the only way to recover is to rebuild the original Arduino software from [here](https://github.com/ralphxavier/SI4735) - which I have not tested and which may not work - see disclaimer at that repository.* 41 | 42 | There are two steps to installing the software. 43 | 44 | 1) Install modified Espruino interpreter from this repository using ```esptool```. 45 | The esptool paramemters can be found in [README_flash.txt](../espruino_2v13.11_esp32/README_flash.txt). 46 | For further information on installing Espruino on the ESP32 see the [Espruino Website](https://www.espruino.com/ESP32). 47 | 48 | After loading the binaries and resetting the ESP32, it should now be possible to connect to the ESP32 using the Espruino [WebIDE](https://www.espruino.com/ide/). - see [here](https://www.espruino.com/Programming) for further information. Note that it requires Web Serial to be enable the browser I use Chrome and Chromium. When you connect, a REPL prompt should appear. 49 | 50 | 2) The next step is to load the set of Javascript files using the WebIDE. The following is the list of files, some of which need to be renamed when loading. To load a file click the disk icon - three down - on the central panel of the IDE. 51 | 52 | ``` 53 | boot0.js -> .boot0 54 | bootcde.js -> .bootcde 55 | encode.js 56 | // all the javascript file in the ats100 folder 57 | ats100/smallradio.png -> smallradio // choose 4bit MAc pallette to convert in WebIDE. 58 | ``` 59 | 60 | This is rather laborious as the current Espruino app loader does not support Web Serial. 61 | 62 | ## DISCLAIMER 63 | 64 | There is no guarantee that this will work on your ATS100 without modification. 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /ats100/amapp.js: -------------------------------------------------------------------------------- 1 | eval(STOR.read("selector.js")); 2 | eval(STOR.read("button.js")); 3 | eval(STOR.read("bardisp.js")); 4 | eval(STOR.read("freqdisp.js")); 5 | 6 | var VOL=32; 7 | var BRIGHT=40; 8 | var SCREENSAVE=120; 9 | var STATE=0; //0 = SELECT, 1 = VOL, 2= FREQ, 3 = STATION, 4 = BRIGHTNESS 10 | var FREQ = 198; 11 | var RSSI =0; 12 | var SNR =0; 13 | var BANDNAME ="LW"; 14 | var LOWBAND = 130; 15 | var HIGHBAND = 279; 16 | var STEP = 9; 17 | var BWindex = 1; 18 | var CAP =0; 19 | 20 | 21 | var BANDS = (STOR.readJSON("bands.json")||[]).filter((e)=>{return e.mod=="AM";}); 22 | 23 | var BANDSEL = new Selector(BANDS,148,83,(b)=>{STATE = b?3:0;}); 24 | var VOLDISP = new BarDisp("Vol:",28,62,VOL,(b)=>{STATE = b?1:0;}); 25 | var BRIGHTDISP = new BarDisp("BL :",28,72,BRIGHT,(b)=>{STATE = b?4:0;}); 26 | var FREQDISP = new FreqDisp("KHz",50,19,115,28,0,5,FREQ,(b)=>{STATE = b?2:0;},(f)=>{findBand(f);}); 27 | var SCANUP = new Button("Scan+",0, 111, 44, 23, ()=>{scan(true,SCANUP,SCANDOWN);},12); 28 | var SCANDOWN = new Button("Scan-",49, 111, 44, 23, ()=>{scan(false,SCANDOWN,SCANUP);},12); 29 | var STEPSET = new Button("9", 0,94, 44, 12,(b)=>{changeStep(b);},8); 30 | var BWSET = new Button("4.0", 49,94, 44, 12,(b)=>{changeBW(b);},8); 31 | var AGCCTL = new Button("agc on", 98,94, 44, 12,(b)=>{changeAGC(b);},8); 32 | var ITEMS=[ 33 | FREQDISP, VOLDISP,BRIGHTDISP, STEPSET, BWSET, AGCCTL, BANDSEL,SCANUP, SCANDOWN, 34 | new Button("Mute" ,98,111, 44, 23, (b)=>{RADIO.mute(b);},12), 35 | ]; 36 | 37 | var agcindex = 0; 38 | const agcstr = ["agc on", "off", "low", "med", "high"] 39 | function changeAGC(b){ 40 | if (!b) return; 41 | agcindex = (agcindex+1)%5; 42 | AGCCTL.str = agcstr[agcindex]; 43 | if (agcindex==0) 44 | RADIO.setAGC(true,0); 45 | else 46 | RADIO.setAGC(false,(agcindex-1)*3); 47 | AGCCTL.reset(); 48 | } 49 | 50 | var stepindex= 2; 51 | const steps =[1,5,9,10]; 52 | function changeStep(b){ 53 | if (!b) return; 54 | stepindex = (stepindex+1)%4; 55 | STEP=steps[stepindex]; 56 | STEPSET.str = STEP.toString(); 57 | STEPSET.reset(); 58 | } 59 | 60 | const bwidss =[6,4,3,2,1,1.8,2.5]; 61 | function changeBW(b,index){ 62 | if (!b) return; 63 | if (typeof index !== 'undefined') BWindex=index; else BWindex = (BWindex+1)%7; 64 | RADIO.setProp(0x3102,BWindex); 65 | BWSET.str = bwidss[BWindex].toFixed(1); 66 | BWSET.reset(); 67 | } 68 | 69 | function drawBand() { 70 | g.setColor(Yellow).setFont('6x8').setFontAlign(-1,-1); 71 | g.drawString("MIN: "+LOWBAND+"KHz ",148,62,true); 72 | g.drawString("MAX: "+HIGHBAND+"KHz ",148,72,true); 73 | STEPSET.str = STEP.toString(); STEPSET.draw(); 74 | } 75 | 76 | function drawSignal(){ 77 | g.setColor(Yellow); 78 | g.setFont('6x8').setFontAlign(-1,-1).drawString("RSSI: "+RSSI+" ",0,0,true); 79 | g.setFontAlign(-1,-1).drawString("SNR : "+SNR+" ",95,0,true); 80 | } 81 | 82 | 83 | function setBand(f) { 84 | if (BANDS.length!=0) { 85 | var bd = BANDSEL.selected(); 86 | BANDNAME=bd.name; 87 | LOWBAND =bd.min; 88 | HIGHBAND=bd.max; 89 | STEP=bd.step; 90 | if (f) FREQ=f; else FREQ=bd.freq; 91 | CAP= (bd.name=="LW" || bd.name=="MW")?0:1; 92 | } 93 | drawBand(); 94 | RADIO.setProp(0x3400,LOWBAND); 95 | RADIO.setProp(0x3401,HIGHBAND); 96 | RADIO.setProp(0x3402,STEP); 97 | setTune(FREQ); 98 | } 99 | 100 | function findBand(f){ 101 | var bi = BANDS.findIndex((e)=>{return f<=e.max && f>=e.min;}); 102 | if (bi>=0) { 103 | BANDSEL.pos=bi; 104 | BANDSEL.draw(true); 105 | setBand(f); 106 | } 107 | } 108 | 109 | function setTune(f){ 110 | RADIO.tuneAM(f,CAP); 111 | while(!RADIO.endTune()); 112 | var r= RADIO.getTuneStatus(); 113 | FREQ=r.freq; SNR=r.snr; RSSI=r.rssi; 114 | FREQDISP.update(FREQ); 115 | drawSignal(); 116 | } 117 | 118 | var SCANNER=null; 119 | 120 | function scan(up,thisone,other){ 121 | if (SCANNER) SCANNER=clearInterval(SCANNER); 122 | if(other.press){other.reset();} 123 | RADIO.seek(up,false); 124 | SCANNER=setInterval(()=>{ 125 | if (!RADIO.endTune()) return; 126 | if (SCANNER) SCANNER=clearInterval(SCANNER); 127 | var r=RADIO.getTuneStatus(); 128 | FREQ=r.freq; SNR=r.snr; RSSI=r.rssi; 129 | FREQDISP.update(FREQ); drawSignal(); 130 | thisone.reset(); 131 | },100); 132 | } 133 | 134 | function initRADIO(){ 135 | RADIO.reset(); 136 | RADIO.powerAM(true); 137 | RADIO.setProp(0xFF00,0); //turn off debug see AN332 re noise 138 | RADIO.setProp(0x3102,BWindex); 139 | RADIO.setProp(0x3302,0); 140 | RADIO.volume(VOL); 141 | setBand(); 142 | RADIO.setAGC(true,0); 143 | } 144 | 145 | var prevpos =0; 146 | var position=0; 147 | 148 | function move(inc){ 149 | function mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 150 | position=mod(position+inc,ITEMS.length); 151 | ITEMS[prevpos].focus(false); 152 | ITEMS[position].focus(true); 153 | prevpos=position; 154 | } 155 | 156 | function setControls(){ 157 | ROTARY.handler = (inc) => { 158 | if (SCREENSAVE<=0) 159 | brightness(BRIGHT/63); 160 | else if (FREQDISP.edit) 161 | FREQDISP.adjust(inc); 162 | else if (STATE==0) 163 | move(inc); 164 | else if (STATE==2){ 165 | FREQ+=(inc*STEP); 166 | FREQ = FREQHIGHBAND?HIGHBAND:FREQ; 167 | setTune(FREQ); 168 | } else if(STATE==1) { 169 | VOL+=inc*4; 170 | VOL=VOL<0?0:VOL>63?63:VOL; 171 | VOLDISP.update(VOL); 172 | RADIO.volume(VOL); 173 | } else if(STATE==4) { 174 | BRIGHT+=inc*4; 175 | BRIGHT=BRIGHT<5?5:BRIGHT>63?63:BRIGHT; 176 | BRIGHTDISP.update(BRIGHT); 177 | brightness(BRIGHT/63); 178 | } else if (STATE==3) { 179 | BANDSEL.move(inc); 180 | if (BANDS.length!=0) setBand(); 181 | } 182 | SCREENSAVE = 120; 183 | }; 184 | ROTARY.on("change",ROTARY.handler); 185 | BUTTON.on("change",(d)=>{ 186 | if (SCREENSAVE<=0) brightness(BRIGHT/63); else ITEMS[position].toggle(d); 187 | SCREENSAVE = 120; 188 | }); 189 | BUTTON.on("doubleclick",()=>{if (FREQDISP.focusd) FREQDISP.onDclick();}); 190 | } 191 | 192 | var s; 193 | 194 | function restoreState(){ 195 | s = STOR.readJSON("amstate.json",1)||{frequency:1024, volume:32, bright:40, bandwidth:1}; 196 | VOL=s.volume; VOLDISP.update(VOL); RADIO.volume(VOL); 197 | BRIGHT=s.bright; BRIGHTDISP.update(BRIGHT); brightness(BRIGHT/63); 198 | changeBW(true,s.bandwidth); 199 | findBand(s.frequency); 200 | } 201 | 202 | function saveState(){ 203 | s.frequency=FREQ; 204 | s.volume=VOL; 205 | s.bright=BRIGHT; 206 | s.bandwidth=BWindex; 207 | STOR.writeJSON("amstate.json",s); 208 | } 209 | 210 | E.on("kill",saveState); 211 | 212 | g.clear(); 213 | for (var i=0;i{ 220 | var r = RADIO.getSQ(); 221 | SNR=r.snr; RSSI=r.rssi; STEREO=r.stereo; 222 | drawSignal(); 223 | g.setFontAlign(-1,-1).drawString("BAT: "+getBattery().toFixed(1)+"V",180,0,true); 224 | if (SCREENSAVE>0) { 225 | --SCREENSAVE; 226 | if (SCREENSAVE<=0){ 227 | brightness(0); 228 | } 229 | } 230 | },1000); -------------------------------------------------------------------------------- /ats100/bardisp.js: -------------------------------------------------------------------------------- 1 | class BarDisp { 2 | constructor(str,x,y,v,fn){ 3 | this.x=x; this.y=y; 4 | this.str=str; 5 | this.v=v; 6 | this.old=0; 7 | this.press=false; 8 | this.focusd = false; 9 | this.press = false; 10 | this.fn = fn; 11 | } 12 | 13 | update(v){ 14 | if (v>=this.old) g.setColor(this.press?Green:Grey).fillRect(this.x,this.y+1,this.x+v,this.y+5); 15 | if (v{load("fmapp.js");},20); 6 | var AMBUTTON = new Button("AM",60,70,50,30,()=>{load("amapp.js");},20); 7 | var SSBUTTON = new Button("SSB",120,70,50,30,()=>{load("ssbapp.js");},20); 8 | var OFFBUTTON = new Button("OFF",180,70,50,30,()=>{load();},20); 9 | 10 | var ITEMS=[FMBUTTON,AMBUTTON,SSBUTTON,OFFBUTTON]; 11 | 12 | var prevpos =3; 13 | var position=3; 14 | 15 | function move(inc){ 16 | function mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 17 | position=mod(position+inc,ITEMS.length); 18 | ITEMS[prevpos].focus(false); 19 | ITEMS[position].focus(true); 20 | prevpos=position; 21 | } 22 | 23 | ROTARY.on("change",(inc)=>{move(inc);}); 24 | BUTTON.on("change",(d)=>{ITEMS[position].toggle(d);}); 25 | 26 | g.setColor(-1).setFont("6x8",1).setFontAlign(0,-1).drawString("Long touch to return to this screen.",120,110); 27 | 28 | for (var i=0;i{ 20 | if(a.nameb.name)return 1; 22 | return 0; 23 | }); 24 | 25 | var STATSEL = new Selector(STATIONS,148,83,(b)=>{STATE = b?3:0;}); 26 | var VOLDISP = new BarDisp("Vol:",28,72,VOL,(b)=>{STATE = b?1:0;}); 27 | var BRIGHTDISP = new BarDisp("Bright:",170,72,BRIGHT,(b)=>{STATE = b?4:0;}); 28 | var FREQDISP = new FreqDisp("MHz",110,19,92,28,1,4,FREQ/10,(b)=>{STATE = b?2:0;},(f)=>{setTune(f*10);}); 29 | var SCANUP = new Button("Scan+",0, 83, 44, 23, ()=>{scan(true,SCANUP,SCANDOWN);},12); 30 | var SCANDOWN = new Button("Scan-",49, 83, 44, 23, ()=>{scan(false,SCANDOWN,SCANUP);},12); 31 | var ADDSTAT = new Button("Add", 98,83, 44, 23,(b)=>{addStation(b);},12); 32 | var DELSTAT = new Button("Del", 98,111, 44, 23,(b)=>{delStation(b);},12); 33 | var ITEMS=[ 34 | FREQDISP, VOLDISP,BRIGHTDISP, SCANUP, SCANDOWN, ADDSTAT, STATSEL, DELSTAT, 35 | new Button("Mute" ,49,111, 44, 23, (b)=>{RADIO.mute(b);},12), 36 | new Button("RDS", 0, 111, 44, 23, (b)=>{if (b) rdsStart(); else rdsStop();},12), 37 | ]; 38 | 39 | function addStation(b){ 40 | if(b && SEGMENTS==15) { 41 | STATSEL.add(getname(),FREQ); 42 | STATSEL.draw(1); 43 | STOR.writeJSON("stations.json",STATIONS); 44 | } 45 | ADDSTAT.reset(); 46 | } 47 | 48 | function delStation(b){ 49 | if(b && DELSTAT.press) { 50 | STATSEL.del(); 51 | STATSEL.draw(1); 52 | if (STATIONS.length!=0) setTune(STATSEL.freq()); 53 | STOR.writeJSON("stations.json",STATIONS); 54 | } 55 | DELSTAT.reset(); 56 | } 57 | 58 | function drawSignal(){ 59 | g.setColor(Yellow); 60 | g.setFont('6x8').setFontAlign(-1,-1).drawString("RSSI: "+RSSI+" ",0,0,true); 61 | g.setFontAlign(0,-1).drawString(STEREO==1?"Stereo":" ",120,0,true); 62 | g.setFontAlign(-1,-1).drawString("SNR : "+SNR+" ",0,10,true); 63 | } 64 | 65 | function setTune(f){ 66 | f = fHIGHBAND?HIGHBAND:f; 67 | RADIO.tune(f); 68 | rdsClear(); 69 | while(!RADIO.endTune()); 70 | var r= RADIO.getTuneStatus(); 71 | FREQ=r.freq; SNR=r.snr; RSSI=r.rssi; 72 | FREQDISP.update(FREQ/10); 73 | drawSignal(); 74 | } 75 | 76 | var SCANNER=null; 77 | 78 | function scan(up,thisone,other){ 79 | if (SCANNER) SCANNER=clearInterval(SCANNER); 80 | if(other.press){other.reset();} 81 | rdsClear(); 82 | RADIO.seek(up,false); 83 | SCANNER=setInterval(()=>{ 84 | if (!RADIO.endTune()) return; 85 | if (SCANNER) SCANNER=clearInterval(SCANNER); 86 | var r=RADIO.getTuneStatus(); 87 | FREQ=r.freq; SNR=r.snr; RSSI=r.rssi; 88 | FREQDISP.update(FREQ/10); drawSignal(); 89 | thisone.reset(); 90 | },100); 91 | } 92 | 93 | function initRADIO(){ 94 | RADIO.reset(); 95 | RADIO.powerFM(true); 96 | RADIO.setProp(0xFF00,0); //turn off debug see AN332 re noise 97 | //RADIO.setProp(0x1800,127); // set to blend set to mono 98 | //RADIO.setProp(0x1801,127); // mono blend threshold - force mono 99 | RADIO.volume(VOL); 100 | } 101 | 102 | var prevpos =0; 103 | var position=0; 104 | 105 | function move(inc){ 106 | function mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 107 | position=mod(position+inc,ITEMS.length); 108 | ITEMS[prevpos].focus(false); 109 | ITEMS[position].focus(true); 110 | prevpos=position; 111 | } 112 | 113 | function setControls(){ 114 | ROTARY.handler = (inc) => { 115 | if (SCREENSAVE<=0) 116 | brightness(BRIGHT/63); 117 | else if (FREQDISP.edit) 118 | FREQDISP.adjust(inc); 119 | else if (STATE==0) { 120 | move(inc); 121 | } else if (STATE==2){ 122 | FREQ+=(inc*10); 123 | setTune(FREQ); 124 | } else if(STATE==1) { 125 | VOL+=inc*4; 126 | VOL=VOL<0?0:VOL>63?63:VOL; 127 | VOLDISP.update(VOL); 128 | RADIO.volume(VOL); 129 | } else if(STATE==4) { 130 | BRIGHT+=inc*4; 131 | BRIGHT=BRIGHT<5?5:BRIGHT>63?63:BRIGHT; 132 | BRIGHTDISP.update(BRIGHT); 133 | brightness(BRIGHT/63); 134 | } else if (STATE==3) { 135 | STATSEL.move(inc); 136 | if (STATIONS.length!=0) setTune(STATSEL.freq()); 137 | } 138 | SCREENSAVE = 120; 139 | }; 140 | ROTARY.on("change",ROTARY.handler); 141 | BUTTON.on("change",(d)=>{ 142 | if (SCREENSAVE<=0) brightness(BRIGHT/63); else ITEMS[position].toggle(d); 143 | SCREENSAVE = 120; 144 | }); 145 | BUTTON.on("doubleclick",()=>{if (FREQDISP.focusd) FREQDISP.onDclick();}); 146 | } 147 | 148 | var s; 149 | 150 | function findStation(f){ 151 | var bi = STATIONS.findIndex((e)=>{return f==e.freq;}); 152 | if (bi>=0) { 153 | STATSEL.pos=bi; 154 | STATSEL.draw(); 155 | } 156 | setTune(f); 157 | } 158 | 159 | function restoreState(){ 160 | s = STOR.readJSON("fmstate.json",1)||{frequency:9540, volume:32, bright:40}; 161 | VOL=s.volume; VOLDISP.update(VOL); RADIO.volume(VOL); 162 | BRIGHT=s.bright; BRIGHTDISP.update(BRIGHT); brightness(BRIGHT/63); 163 | findStation(s.frequency); 164 | } 165 | 166 | function saveState(){ 167 | s.frequency=FREQ; 168 | s.volume=VOL; 169 | s.bright=BRIGHT; 170 | STOR.writeJSON("fmstate.json",s); 171 | } 172 | 173 | E.on("kill",saveState); 174 | 175 | g.clear(); 176 | for (var i=0;i{ 182 | var r = RADIO.getSQ(); 183 | SNR=r.snr; RSSI=r.rssi; STEREO=r.stereo; 184 | drawSignal(); 185 | g.setFontAlign(-1,-1).drawString("BAT: "+getBattery().toFixed(1)+"V",180,0,true); 186 | if (SCREENSAVE>0) { 187 | --SCREENSAVE; 188 | if (SCREENSAVE<=0){ 189 | brightness(0); 190 | } 191 | } 192 | },1000); -------------------------------------------------------------------------------- /ats100/freqdisp.js: -------------------------------------------------------------------------------- 1 | Graphics.prototype.setFontDSEG7ClassicMiniBold = function(scale) { 2 | // Actual height 28 (27 - 0) 3 | this.setFontCustom( 4 | E.toString(require('heatshrink').decompress(atob('AAVwAQM+AQMfAQMPwEAg/ggEB+AJBnAiOgf/z/+j/w//M/0H/YgBgF/FQQkC/4GX8/An18FIQwC4Y3CT34ACvyTBLYM/wP/w6EB/n/QAQATn5oBgKgBgHPUAMeUAMB8AvBAwP/Ay/n7kAUAPAg6gBbATzBADdAg0Ai0A+EDvEPgF3wHwg4GYn3j8dn+1/yH/x6gB/v/KoIASg//UQS6BgP84AHBQgIHBYoQGaj+cvkD/nP+F/8P/4//bATvU/DvCAwNP8ygkgHh+dgg0/6CGBJgI1BUCkDNAUf+P/5n+5/78CEBv6EBh/wAwP/Ay/gjk+vkATYI2B/7PBUC0f/gdCwAkBNoMOBoRzCQwIGZh18gPw5/gn+B/6gBn/9//gUH4GB8/cvl8FIP3GAXD/+/UCs//EAgLBCp/mgEWQQMDvCJBu+A+EHAzE+8fjs/2v+Q/+PQgKgBWIQALjkB4ByBngGDCAwA='))), 5 | 46, 6 | atob("AAgXFxcXFxcXFxcXBg=="), 7 | 28+(scale<<8)+(1<<16) 8 | ); 9 | return this; 10 | }; 11 | 12 | class FreqDisp { 13 | constructor(str,x,y,w,fz,d,nd,f,fn,fn2){ 14 | this.x=x; this.y=y; this.w = w; this.fz = fz; 15 | this.decpoint = d; 16 | this.str=str; 17 | this.f=f; 18 | this.press=false; 19 | this.focusd = false; 20 | this.edit = false; 21 | this.editDigit=false; 22 | this.ndigits=nd; 23 | this.digits = new Array(this.ndigits); 24 | this.curd = this.ndigits-1; 25 | this.fn = fn; 26 | this.fn2 = fn2; 27 | this.buf = Graphics.createArrayBuffer(w,fz+4,1,{msb:true}); 28 | } 29 | 30 | update(f){ 31 | this.f=f; 32 | this.buf.clear(); 33 | if (this.edit){ 34 | var lxd = this.w-24-23*this.curd; 35 | this.buf.drawRect(lxd<0?0:lxd,0,lxd+23,this.fz+3); 36 | for (var i = 0; i0) { 44 | var dx = this.w-2-23*this.decpoint; 45 | this.buf.fillRect(dx,25,dx+2,27); 46 | } 47 | g.setBgColor(this.focusd?DarkGrey:0).setColor(this.press?-1:Cyan).drawImage(this.buf,this.x,this.y); 48 | g.setBgColor(0); 49 | } 50 | 51 | draw(){ 52 | g.setColor(this.press?-1:Cyan).setFont("Vector",14).setFontAlign(-1,1).drawString(this.str,this.x+this.w,this.y+this.fz-4); 53 | this.update(this.f); 54 | } 55 | 56 | focus(b){ 57 | this.focusd = b; 58 | this.draw(); 59 | } 60 | toggle(b){ 61 | if (b){ 62 | this.press=!this.press; 63 | this.editDigit = !this.editDigit; 64 | this.draw(); 65 | } else { 66 | if (!this.edit) this.fn(this.press); 67 | } 68 | } 69 | 70 | startEdit(){ 71 | this.edit=true; 72 | for (var i=0; i=1) { 77 | this.digits[d]= ac % 10; 78 | ac = (ac-this.digits[d])/10; 79 | ++d; 80 | } 81 | this.draw(); 82 | } 83 | 84 | endEdit(){ 85 | this.edit=!true; 86 | var res = 0; 87 | for (var i=this.ndigits-1; i>=0;--i){ 88 | res = res*10 + this.digits[i]; 89 | } 90 | this.f = res; 91 | this.draw(); 92 | } 93 | 94 | onDclick(){ 95 | if (!this.edit) 96 | this.startEdit(); 97 | else { 98 | this.endEdit(); 99 | this.fn2(this.f); 100 | } 101 | } 102 | 103 | adjust(inc){ 104 | function mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 105 | if (!this.editDigit){ 106 | this.curd=mod(this.curd+inc,this.ndigits); 107 | } else { 108 | this.digits[this.curd]=mod(this.digits[this.curd]+inc,10); 109 | } 110 | this.draw(); 111 | } 112 | 113 | } 114 | 115 | 116 | -------------------------------------------------------------------------------- /ats100/freqdispold.js: -------------------------------------------------------------------------------- 1 | class FreqDisp { 2 | constructor(str,x,y,w,fz,f,fn){ 3 | this.x=x; this.y=y; this.w = w; this.fz = fz; 4 | this.str=str; 5 | this.f=f; 6 | this.press=false; 7 | this.focusd = false; 8 | this.press = false; 9 | this.fn = fn; 10 | this.buf = Graphics.createArrayBuffer(w,fz+2,1,{msb:true}); 11 | } 12 | 13 | update(f){ 14 | this.f=f; 15 | this.buf.clear(); 16 | this.buf.setFont("Vector",this.fz).setFontAlign(1,1).drawString((f/100).toFixed(1),this.w-1,this.fz-2); 17 | g.setColor(this.press?-1:Cyan).drawImage(this.buf,this.x,this.y); 18 | } 19 | 20 | draw(){ 21 | g.setColor(this.press?-1:Cyan).setFont("Vector",14).setFontAlign(-1,1).drawString(this.str,this.x+this.w,this.y+this.fz-4); 22 | this.update(this.f); 23 | g.setColor(this.focusd?Cyan:0).drawLine(this.x,this.y+this.fz+1,this.x+this.w,this.y+this.fz+1); 24 | } 25 | 26 | focus(b){ 27 | this.focusd = b; 28 | this.draw(); 29 | } 30 | toggle(){ 31 | this.press=!this.press; 32 | this.draw(); 33 | this.fn(this.press); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ats100/main.js: -------------------------------------------------------------------------------- 1 | var STOR = require("Storage"); 2 | eval(STOR.read("st7789v.js")); 3 | var g = ST7789(); 4 | brightness(40/63); 5 | const Grey = g.toColor(0.8,0.8,0.8); 6 | const DarkGrey = g.toColor(0.3,0.3,0.3); 7 | const Green = g.toColor(0,0.7,0); 8 | const Yellow = g.toColor(1,1,0); 9 | const Blue = g.toColor(0,0,1); 10 | const Cyan = g.toColor(0,1,1); 11 | eval(STOR.read("encoder.js")); 12 | var ROTARY = createEncoder(D33,D32); 13 | eval(STOR.read("si4735.js")); 14 | RADIO.reset(); 15 | 16 | function createSwitch(pinA){ 17 | pinMode(pinA,"input_pullup"); 18 | var OBJ = {}; 19 | var lastpush; 20 | var TO; 21 | 22 | function handler(ns){ 23 | if (TO) TO = clearTimeout(TO); 24 | if (!ns.state && (ns.time-lastpush)<0.5) 25 | OBJ.emit("doubleclick"); 26 | else 27 | OBJ.emit("change",!ns.state); 28 | if(!ns.state) { 29 | lastpush=ns.time; 30 | TO = setTimeout(()=>{OBJ.emit("longpush");},1500); 31 | } 32 | } 33 | setWatch(handler,pinA,{repeat:true,edge:"both",debounce:25}); 34 | return OBJ; 35 | } 36 | 37 | var BUTTON = createSwitch(D2); 38 | function getBattery() {return 7.24 * analogRead(D35);} 39 | 40 | BUTTON.on("longpush",()=>{RADIO.reset();load("chooser.js");}); 41 | -------------------------------------------------------------------------------- /ats100/rds.js: -------------------------------------------------------------------------------- 1 | var STATNAME = new Uint8Array(8); 2 | var SEGMENTS = 0; 3 | var RDSMSG = new Uint8Array(64); 4 | var AB = 0; 5 | 6 | function clearMsg(){ 7 | for(var i=0;i{return e==0 || e==42}); 12 | end = end<0?63:end; 13 | if (end <8) return null; 14 | return end<63 ? E.toString(RDSMSG.subarray(0,end)): E.toString(RDSMSG); 15 | } 16 | 17 | function rdsInit(){ 18 | RADIO.setProp(0x1500,0x0001); // enable receive interrupt 19 | RADIO.setProp(0x1501,0x0004); // set fifo theshold for interrupt 20 | RADIO.setProp(0x1502,0xAA01); // set error correction and enable 21 | } 22 | 23 | function getname(){ 24 | if (SEGMENTS==15) { 25 | return E.toString(STATNAME); 26 | } else return null; 27 | } 28 | 29 | function drawStation(s){ 30 | if (s) g.setColor(-1).setFont("6x8",2).setFontAlign(-1,-1).drawString(s,0,30,true); 31 | } 32 | 33 | var rdsbuf = Graphics.createArrayBuffer(220,20,1,{msb:true}); 34 | rdsbuf.setFont("6x8",1).setFontAlign(-1,-1); 35 | 36 | function drawMsg(msg){ 37 | rdsbuf.clear(); 38 | if (msg) msg= rdsbuf.drawString(rdsbuf.wrapString(msg, rdsbuf.getWidth()).join("\n"),0,0); 39 | g.setColor(-1).drawImage(rdsbuf,0,51); 40 | } 41 | 42 | function decode(m){ 43 | function cl(c){ c=c&0x7F; return c<32?42:c;} 44 | var g = (m[6]&0xF0)>>4; 45 | var b0 = (m[6]&0x08)>>3; 46 | var o = g==0?m[7]&0x03:m[7]&0x0f; 47 | if (g==2) { 48 | var ab = (m[7]&0x10)>>4; 49 | if (ab!=AB) {AB=ab; clearMsg();} 50 | } 51 | if (g==0){ 52 | STATNAME[2*o]=cl(m[10]); 53 | STATNAME[2*o+1]=cl(m[11]); 54 | SEGMENTS=SEGMENTS|(1<{ 71 | if(RADIO.hasRDS()){ 72 | while (true) { 73 | var dd = RADIO.getRDS(); 74 | if (dd[3]==0) break; 75 | decode(dd); 76 | } 77 | } 78 | var name = getname(); 79 | drawStation(name); 80 | var msg = getMsg(); 81 | drawMsg(msg); 82 | },1000); 83 | } 84 | 85 | function rdsClear(){ 86 | SEGMENTS=0; 87 | clearMsg(); 88 | drawMsg(null); 89 | drawStation(" "); 90 | } 91 | 92 | function rdsStop(){ 93 | rdsClear(); 94 | if (global.RDSPOLL) global.RDSPOLL=clearInterval(global.RDSPOLL); 95 | } 96 | 97 | 98 | -------------------------------------------------------------------------------- /ats100/selector.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class Selector { 5 | constructor(list,x,y,fn){ 6 | this.list=list; 7 | this.px=x; 8 | this.py=y; 9 | this.pos=0; 10 | this.buf = Graphics.createArrayBuffer(90,50,2,{msb:true}); 11 | this.pal = new Uint16Array([Blue,-1,Grey,Green]); 12 | this.buf.setFontAlign(0,0).setColor(2).fillRect(0,0,89,109); 13 | this.focusd = false; 14 | this.fn = fn; 15 | this.press = false; 16 | } 17 | 18 | mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 19 | 20 | draw() { 21 | this.buf.setColor(this.press?3:0).fillRect(0,0,89,49); 22 | if (this.focusd) this.buf.setColor(1).drawRect(0,0,89,49); 23 | function mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 24 | var len = this.list.length; 25 | if (len!=0){ 26 | var bound = len>=3?1:0; 27 | for (var i =-bound;i<=bound;i++) 28 | this.buf.setColor(i==0?1:2).setFont("Vector",14).drawString(this.list[mod(this.pos+i,len)].name,45,25+16*i); 29 | } 30 | g.drawImage({width:90,height:50,bpp:2,palette:this.pal,buffer:this.buf.buffer},this.px,this.py); 31 | } 32 | 33 | focus(b){ 34 | this.focusd = b; 35 | this.draw(); 36 | } 37 | toggle(b){ 38 | if (b){ 39 | this.press=!this.press; 40 | this.draw(); 41 | } else { 42 | this.fn(this.press); 43 | } 44 | } 45 | move(inc){ 46 | if (this.list.length==0) return; 47 | this.pos=this.mod(this.pos+inc,this.list.length); 48 | this.draw(true); 49 | } 50 | 51 | freq(){ 52 | return this.list[this.pos].freq; 53 | } 54 | 55 | selected(){ 56 | return this.list[this.pos]; 57 | } 58 | 59 | add(newname,f){ 60 | if (this.list.length!=0 && newname==this.list[this.pos].name) return; 61 | this.list.splice(this.pos,0,{name:newname,freq:f}); 62 | } 63 | 64 | del(){ 65 | if(this.list.length==0) return; 66 | this.list.splice(this.pos,1); 67 | if (this.pos==this.list.length) --this.pos; 68 | if (this.pos<0) this.pos=0; 69 | } 70 | 71 | } 72 | 73 | -------------------------------------------------------------------------------- /ats100/si4735.js: -------------------------------------------------------------------------------- 1 | //var RADIOI2C = new I2C(); 2 | // var ret = 0; 3 | // while (ret==0) ret = RADIO.read(1)[0]; 4 | // if (ret!=128) return false; 5 | I2C1.setup({scl:D22,sda:D21,bitrate:200000}); 6 | 7 | var RADIO = { 8 | isFM:null, 9 | delayms:(d) => { 10 | var t = getTime()+d/1000; while(getTime() { 13 | I2C1.writeTo(0x63,d); 14 | }, 15 | read:(n) => { 16 | return I2C1.readFrom(0x63,n); 17 | }, 18 | reset:() => { 19 | D25.set(); 20 | digitalPulse(D25,0,10); 21 | RADIO.delayms(30); 22 | 23 | }, 24 | waitCTS:(d)=>{ 25 | for(var i=0;i<100;i++) if(RADIO.read(1)[0]&0x80) return true; 26 | return false; 27 | }, 28 | powerFM:(b)=>{ 29 | if (b) { 30 | RADIO.isFM=true; 31 | RADIO.write([0x01,0x90,0x05]); //FM analogue 32 | } else { 33 | RADIO.write(0x11); 34 | } 35 | RADIO.delayms(500); //stabilisation delay 36 | return RADIO.waitCTS(); 37 | }, 38 | powerAM:(b)=>{ 39 | if (b) { 40 | RADIO.isFM=false; 41 | RADIO.write([0x01,0x91,0x05]); //AM analogue 42 | } else { 43 | RADIO.write(0x11); 44 | } 45 | RADIO.delayms(500); //stabilisation delay 46 | return RADIO.waitCTS(); 47 | }, 48 | powerSSB:(b)=>{ 49 | if (b) { 50 | RADIO.isFM=false; 51 | RADIO.write([0x01,0xB1,0x05]); //AM patch analogue 52 | if (!RADIO.waitCTS()) return false; 53 | var nb = SSB.lenPatch(); 54 | var next = 0; 55 | while(next{ 67 | if (!RADIO.waitCTS()) return; 68 | var cm = new Uint8Array(5); 69 | cm[0] = 0x20; 70 | cm[1] = 0x00; 71 | cm[2] = f>>8; 72 | cm[3] = f & 0xFF; 73 | cm[4] = 0; 74 | RADIO.write(cm); 75 | }, 76 | tuneAM:(f,sw)=>{ 77 | if (!RADIO.waitCTS()) return; 78 | var cm = new Uint8Array(6); 79 | cm[0] = 0x40; 80 | cm[1] = 0x00; 81 | cm[2] = f>>8; 82 | cm[3] = f & 0xFF; 83 | cm[4] = 0; 84 | cm[5] = sw?1:0; 85 | RADIO.write(cm); 86 | }, 87 | tuneSSB:(f,sw,usb)=>{ 88 | if (!RADIO.waitCTS()) return; 89 | var cm = new Uint8Array(6); 90 | cm[0] = 0x40; 91 | cm[1] = usb?0x80:0x40; 92 | cm[2] = f>>8; 93 | cm[3] = f & 0xFF; 94 | cm[4] = 0; 95 | cm[5] = sw?1:0; 96 | RADIO.write(cm); 97 | }, 98 | endTune:()=>{ 99 | if (!RADIO.waitCTS()) return; 100 | RADIO.write(0x14); 101 | var s = RADIO.read(1)[0]; 102 | return (s&1)>0; 103 | }, 104 | getTuneStatus:()=>{ 105 | if (!RADIO.waitCTS()) return; 106 | var cm = new Uint8Array(2); 107 | cm[0] = RADIO.isFM?0x22:0x42; 108 | cm[1] = 0x01; 109 | RADIO.write(cm); 110 | var res = RADIO.read(8); 111 | return {status:res[0],valid:res[1],freq:(res[2]<<8)+res[3],rssi:res[4],snr:res[5]}; 112 | }, 113 | seek:(up,wrap)=>{ 114 | if (!RADIO.waitCTS()) return; 115 | var cm = new Uint8Array(2); 116 | cm[0] = RADIO.isFM?0x21:0x41; 117 | var arg = 0; 118 | if (up) arg = arg|0x08; 119 | if (wrap) arg = arg | 0x04; 120 | cm[1] = arg; 121 | RADIO.write(cm); 122 | }, 123 | getSQ:()=>{ 124 | if (!RADIO.waitCTS()) return; 125 | var cm = new Uint8Array(2); 126 | cm[0] = RADIO.isFM?0x23:0x43; 127 | cm[1] = 0x00; 128 | RADIO.write(cm); 129 | var res = RADIO.read(RADIO.isFM?8:6); 130 | return {status:res[0],valid:res[2],stereo:res[3]>>7,rssi:res[4],snr:res[5]}; 131 | }, 132 | getAGC:()=>{ 133 | if (!RADIO.waitCTS()) return; 134 | var cm = new Uint8Array(1); 135 | cm[0] = 0x47; 136 | RADIO.write(cm); 137 | var res = RADIO.read(3); 138 | return res; 139 | }, 140 | setAGC:(enable,val)=>{ 141 | if (!RADIO.waitCTS()) return; 142 | var cm = new Uint8Array(3); 143 | cm[0] = 0x48; 144 | cm[1] = enable?0:1; 145 | cm[2] = val; 146 | RADIO.write(cm); 147 | }, 148 | getRDS:()=>{ 149 | if (!RADIO.waitCTS()) return; 150 | var cm = new Uint8Array(2); 151 | cm[0] = 0x24; 152 | cm[1] = 0x01; 153 | RADIO.write(cm); 154 | var res = RADIO.read(13); 155 | return res; 156 | }, 157 | hasRDS:()=>{ 158 | if (!RADIO.waitCTS()) return; 159 | RADIO.write(0x14); 160 | var s = RADIO.read(1)[0]; 161 | return (s&4)>0; 162 | }, 163 | setProp:(id,v)=>{ 164 | if (!RADIO.waitCTS()) return; 165 | var cm = new Uint8Array(6); 166 | cm[0] = 0x12; 167 | cm[1] = 0; 168 | cm[2] = id>>8; 169 | cm[3] = id&0xff; 170 | cm[4] = v>>8; 171 | cm[5] = v&0xff; 172 | RADIO.write(cm); 173 | return RADIO.read(1)[0]; 174 | }, 175 | getProp:(id)=>{ 176 | if (!RADIO.waitCTS()) return; 177 | var cm = new Uint8Array(4); 178 | cm[0] = 0x13; 179 | cm[1] = 0; 180 | cm[2] = id>>8; 181 | cm[3] = id&0xff; 182 | RADIO.write(cm); 183 | var res = RADIO.read(4); 184 | return {status:res[0],value:(res[2]<<8)+res[3]}; 185 | }, 186 | volume:(v)=>{RADIO.setProp(0x4000,v<0?0:v>63?63:v);}, 187 | mute:(b)=>{RADIO.setProp(0x4001,b?3:0);} 188 | }; -------------------------------------------------------------------------------- /ats100/smallradio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/ats100/smallradio.png -------------------------------------------------------------------------------- /ats100/ssbapp.js: -------------------------------------------------------------------------------- 1 | eval(STOR.read("selector.js")); 2 | eval(STOR.read("button.js")); 3 | eval(STOR.read("bardisp.js")); 4 | eval(STOR.read("freqdisp.js")); 5 | eval(STOR.read("stepdisp.js")); 6 | 7 | var VOL=32; 8 | var BRIGHT=40; 9 | var SCREENSAVE=120; 10 | var STATE=0; //0 = SELECT, 1 = VOL, 2= FREQ, 3 = STATION, 4 = BRIGHTNESS, 5= STEPSET 11 | var TUNEORSTEP = false; 12 | var TUNEDFREQ=3303000; //Hz 13 | var FREQ = 0; //x 10000 Hz 14 | var BFO = 0; 15 | var RSSI =0; 16 | var SNR =0; 17 | var LOWBAND = 4700; 18 | var HIGHBAND = 5500; 19 | var MOD="USB"; 20 | var SSB_MODE=0x9001; //AFC disable, AVC enable, bandpass&cutoff, 2.2Khz BW 21 | var STEP = 5000; 22 | var BWindex = 3; 23 | var CAP =1; 24 | 25 | 26 | var BANDS = (STOR.readJSON("bands.json")||[]).filter((e)=>{return e.mod!="AM";}); 27 | 28 | var BANDSEL = new Selector(BANDS,148,83,(b)=>{STATE = b?3:0;}); 29 | var VOLDISP = new BarDisp("Vol:",28,83,VOL,(b)=>{STATE = b?1:0;}); 30 | var BRIGHTDISP = new BarDisp("BL :",28,93,BRIGHT,(b)=>{STATE = b?4:0;}); 31 | var FREQDISP = new FreqDisp("KHz",20,19,161,28,2,7,TUNEDFREQ/10,(b)=>{STATE = b?2:0;},(f)=>{findBand(f/100);}); 32 | var STEPDISP = new StepDisp(90,52,4,(b)=>{STATE = b?5:0;}); 33 | var MODSET = new Button(MOD,0,111,44,23,(b)=>{changeSideBand(b);},12); 34 | var BWSET = new Button("4.0",49,111,44,23,(b)=>{changeBW(b);},12); 35 | var MUTE = new Button("Mute" ,98,111, 44, 23, (b)=>{RADIO.mute(b);},12); 36 | var ITEMS=[FREQDISP, STEPDISP, VOLDISP,BRIGHTDISP, MODSET, BWSET, MUTE,BANDSEL]; 37 | 38 | function setModulation(){ 39 | if (MOD=="SYN") SSB_MODE = SSB_MODE & 0x7fff; 40 | else SSB_MODE = SSB_MODE | 0x8000; 41 | RADIO.setProp(0x0101,SSB_MODE); 42 | MODSET.str=MOD; 43 | MODSET.reset(); 44 | } 45 | 46 | function changeSideBand(b){ 47 | if (!b) return; 48 | MOD = (MOD=="USB")?"LSB":(MOD=="LSB")?"SYN":(MOD=="SYN")?"USB":"USB"; 49 | setModulation(); 50 | setTune(TUNEDFREQ,true); //force tune to change side band 51 | } 52 | 53 | const bwidss =[1.2,2.2,3,4,0.5,1]; 54 | function changeBW(b,index){ 55 | if (!b) return; 56 | if (typeof index !== 'undefined') BWindex=index; else BWindex = (BWindex+1)%6; 57 | var pat = bwidss[BWindex]<2.5 ? BWindex : 0x10 | BWindex; 58 | SSB_MODE = (SSB_MODE & 0xFF00) | pat; 59 | RADIO.setProp(0x0101,SSB_MODE); 60 | BWSET.str = bwidss[BWindex].toFixed(1); 61 | BWSET.reset(); 62 | } 63 | 64 | function drawBand() { 65 | g.setColor(Yellow).setFont('6x8').setFontAlign(-1,-1); 66 | g.drawString("MIN: "+LOWBAND+"KHz ",148,62,true); 67 | g.drawString("MAX: "+HIGHBAND+"KHz ",148,72,true); 68 | } 69 | 70 | function drawSignal(){ 71 | g.setColor(Yellow); 72 | g.setFont('6x8').setFontAlign(-1,-1).drawString("RSSI: "+RSSI+" ",0,0,true); 73 | g.setFontAlign(-1,-1).drawString("SNR : "+SNR+" ",95,0,true); 74 | } 75 | 76 | function setBand(fkhz) { 77 | if (BANDS.length!=0) { 78 | var bd = BANDSEL.selected(); 79 | LOWBAND =bd.min; 80 | HIGHBAND=bd.max; 81 | if (fkhz) TUNEDFREQ=fkhz*1000; else TUNEDFREQ=bd.freq*1000; 82 | MOD = bd.mod; 83 | setModulation(); 84 | CAP= (bd.name=="LOW")?0:1; 85 | } 86 | drawBand(); 87 | setTune(TUNEDFREQ); 88 | } 89 | 90 | function setTune(f,b){ 91 | if (f<153000) return; 92 | var newf = 2*Math.round(f/20000); 93 | BFO = f - newf* 10000; 94 | if (newf!=FREQ || b){ 95 | FREQ = newf; 96 | RADIO.tuneSSB(FREQ*10,CAP,MOD=="USB"); 97 | for(var i=0;i<500;i++) { 98 | if(RADIO.endTune()) break; 99 | } 100 | if (!RADIO.endTune()){ 101 | print("ERROR - setTune timeout"); 102 | return; 103 | } 104 | var r= RADIO.getTuneStatus(); 105 | SNR=r.snr; RSSI=r.rssi; 106 | drawSignal(); 107 | } 108 | RADIO.setProp(0x100,-BFO); 109 | //console.log("TUNEDFREQ ",TUNEDFREQ,"FREQ ",FREQ,"BFO ",BFO); 110 | TUNEDFREQ=f; 111 | FREQDISP.update(f/10); 112 | } 113 | 114 | function initRADIO(){ 115 | RADIO.reset(); 116 | RADIO.powerSSB(false); 117 | if(!RADIO.powerSSB(true)) return false; 118 | RADIO.setProp(0x0101,SSB_MODE); 119 | RADIO.volume(VOL); 120 | return true; 121 | } 122 | 123 | function findBand(f){ 124 | var bi = BANDS.findIndex((e)=>{return f<=e.max && f>=e.min;}); 125 | if (bi>=0) { 126 | BANDSEL.pos=bi; 127 | BANDSEL.draw(true); 128 | setBand(f); 129 | } 130 | } 131 | 132 | var prevpos =0; 133 | var position=0; 134 | 135 | function move(inc){ 136 | function mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 137 | position=mod(position+inc,ITEMS.length); 138 | ITEMS[prevpos].focus(false); 139 | ITEMS[position].focus(true); 140 | prevpos=position; 141 | } 142 | 143 | function setControls(){ 144 | ROTARY.handler = (inc) => { 145 | if (SCREENSAVE<=0) 146 | brightness(BRIGHT/63); 147 | else if (FREQDISP.edit) 148 | FREQDISP.adjust(inc); 149 | else if (STATE==0) { 150 | move(inc); 151 | } else if (STATE==2){ 152 | TUNEDFREQ+=(inc*STEP); 153 | setTune(TUNEDFREQ); 154 | } else if(STATE==1) { 155 | VOL+=inc*4; 156 | VOL=VOL<0?0:VOL>63?63:VOL; 157 | VOLDISP.update(VOL); 158 | RADIO.volume(VOL); 159 | } else if(STATE==4) { 160 | BRIGHT+=inc*4; 161 | BRIGHT=BRIGHT<5?5:BRIGHT>63?63:BRIGHT; 162 | BRIGHTDISP.update(BRIGHT); 163 | brightness(BRIGHT/63); 164 | } else if (STATE==3) { 165 | BANDSEL.move(inc); 166 | if (BANDS.length!=0) setBand(); 167 | } else if (STATE==5){ 168 | STEPDISP.move(inc); 169 | STEP=STEPDISP.step(); 170 | } 171 | SCREENSAVE = 120; 172 | }; 173 | ROTARY.on("change",ROTARY.handler); 174 | BUTTON.on("change",(d)=>{ 175 | if (SCREENSAVE<=0) brightness(BRIGHT/63); else ITEMS[position].toggle(d); 176 | SCREENSAVE = 120; 177 | }); 178 | BUTTON.on("doubleclick",()=>{if (FREQDISP.focusd) FREQDISP.onDclick();}); 179 | } 180 | 181 | var s; 182 | 183 | function restoreState(){ 184 | s = STOR.readJSON("ssbstate.json",1)||{frequency:5505, volume:32, bright:40, bandwidth:3}; 185 | VOL=s.volume; VOLDISP.update(VOL); RADIO.volume(VOL); 186 | BRIGHT=s.bright; BRIGHTDISP.update(BRIGHT); brightness(BRIGHT/63); 187 | changeBW(true,s.bandwidth); 188 | findBand(s.frequency); 189 | } 190 | 191 | function saveState(){ 192 | s.frequency=TUNEDFREQ/1000; 193 | s.volume=VOL; 194 | s.bright=BRIGHT; 195 | s.bandwidth=BWindex; 196 | STOR.writeJSON("ssbstate.json",s); 197 | } 198 | 199 | E.on("kill",saveState); 200 | 201 | g.clear().setColor(-1).setFont("Vector",18).drawString("Loading SSB patch ...",10,70); 202 | if (initRADIO()){ 203 | g.clear(); 204 | for (var i=0;i{ 210 | var r = RADIO.getSQ(); 211 | SNR=r.snr; RSSI=r.rssi; STEREO=r.stereo; 212 | drawSignal(); 213 | g.setFontAlign(-1,-1).drawString("BAT: "+getBattery().toFixed(1)+"V",180,0,true); 214 | if (SCREENSAVE>0) { 215 | --SCREENSAVE; 216 | if (SCREENSAVE<=0){ 217 | brightness(0); 218 | } 219 | } 220 | },1000); 221 | } else g.clear().setColor(-1).setFont("Vector",24).drawString("FAILED to load SSB patch",10,70); -------------------------------------------------------------------------------- /ats100/st7789v.js: -------------------------------------------------------------------------------- 1 | function ST7789() { 2 | var LCD_WIDTH = 240; 3 | var LCD_HEIGHT = 135; 4 | var XOFF = 40; 5 | var YOFF = 52; 6 | var INVERSE = 1; 7 | var ROTATE = 5; 8 | var cmd = lcd_spi_unbuf.command; 9 | 10 | function dispinit(rst,fn) { 11 | function delayms(d) {var t = getTime()+d/1000; while(getTime(){g.clear(1).setFont("6x8").drawString("Loading...",20,20);}); 73 | return g; 74 | } 75 | 76 | //var spi = new SPI(); 77 | SPI1.setup({sck:D18, mosi:D19, baud: 20000000}); 78 | 79 | return connect({spi:SPI1, dc:D16, cs:D5, rst:D23}); 80 | } 81 | 82 | var brightness= function(v){ 83 | v = v>1?1:v<0?0:v; 84 | if (v==0||v==1) 85 | digitalWrite(D4,v); 86 | else 87 | analogWrite(D4,v,{freq:60}); 88 | }; 89 | 90 | /* Test Code 91 | brightness(1); 92 | var g = ST7789(); 93 | g.setColor(1,0,0); 94 | g.fillRect(0,0,239,134); 95 | g.setColor(1,1,1); 96 | g.drawLine(0,0,239,134); 97 | g.drawLine(0,134,239,0); 98 | */ -------------------------------------------------------------------------------- /ats100/stepdisp.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class StepDisp { 5 | constructor(x,y,d,fn){ 6 | this.steps=[10000,5000,1000,100,10]; 7 | this.x=x; 8 | this.y=y; 9 | this.d = d; 10 | this.pos=1; 11 | this.buf = Graphics.createArrayBuffer(23*4,this.d,1,{msb:true}); 12 | this.focusd = false; 13 | this.fn = fn; 14 | this.press = false; 15 | } 16 | 17 | mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 18 | 19 | draw() { 20 | this.buf.clear(); 21 | var sx = this.pos>1?(this.pos-1)*23:this.pos==1?11:0; 22 | this.buf.fillRect(sx,0,sx+16,this.d-1); 23 | g.setBgColor(this.focusd?DarkGrey:0).setColor(this.press?-1:Cyan).drawImage(this.buf,this.x,this.y); 24 | g.setBgColor(0); 25 | } 26 | 27 | focus(b){ 28 | this.focusd = b; 29 | this.draw(); 30 | } 31 | toggle(b){ 32 | if (b){ 33 | this.press=!this.press; 34 | this.draw(); 35 | } else { 36 | this.fn(this.press); 37 | } 38 | } 39 | move(inc){ 40 | if (this.steps.length==0) return; 41 | this.pos=this.mod(this.pos+inc,this.steps.length); 42 | this.draw(true); 43 | } 44 | 45 | step(){ 46 | return this.steps[this.pos]; 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /ats25/README.md: -------------------------------------------------------------------------------- 1 | # ATS-25 Radio programmed with Espruino 2 | 3 | The SI4732 essentially supports three separate radio modes, FM, AM (including SW) and SSB (via a patch). The set of commands and properties supported by these three modes is not the same although there is overlap - for example SSB does not have a scan command. Consequently, to facilitate experimentation and to simplify interfaces, each of the three modes is supported by a different radio app invoked from the home screen shown below: 4 | 5 | ![](../image/home_screen.jpg) 6 | 7 | ## FM Radio 8 | 9 | The FM radio application supports RDS and stations can be saved as presets in the list on the right of the screen shown below using the RDS information. 10 | 11 | ![](../image/fmradio.jpg) 12 | 13 | A video clip of the FM radio can be seen [here](https://www.youtube.com/watch?v=Yk6XuBNn9mA). 14 | 15 | ## AM Radio 16 | 17 | The AM wavebands shown in the window on the right of the screen can be scolled through using the rotary controller. They are defined in a file stored in Espruino storage. This file ```bands.json``` can be easily modified using the Espruino IDE. 18 | 19 | ![](../image/amradio.jpg) 20 | 21 | ## SSB Radio 22 | 23 | There is a short delay - caused by the need to load the SSB patch into the SI4732 - before the SSB radio app launches. The wavebands are stored in the same ```bands.json``` file as for the AM radio. 24 | 25 | ![](../image/ssbradio.jpg) 26 | 27 | The tuning increment can be changed to 10,000Hz, 1000Hz, 100Hz or 10Hz using the rotary controller button - see 1000Hz underline in screen shot. Tuning uses the SSB patch BFO property to minimise 'chuff' on tuning to a different frequency using the tune command. 28 | 29 | ## Direct Frequency Input 30 | 31 | Each of the above three radio apps permits a user to directly input the desired frequency to tune to using the touch screen keypad shown below. This is accessed by swiping down on the screen. 32 | 33 | ![](../image/keys.jpg) 34 | 35 | 36 | ## Technical Challenges 37 | 38 | There were a few technical challenges in getting this to work in Espruino. Firstly, the display and touch controller share the SPI bus and while the screen SPI runs at 20MHz, touch only works with SPI at around 2MHz so you have to re setup SPI each time there is a touch interrupt. This generates a new SPI reference which needs to be passed back to the `lcd_spi_unbuf` driver. I implemented a simple additional function in this driver to pass back the SPI reference. 39 | 40 | Secondly, the rotary controller does two increments every click which is not ideal and the module in the Espruino library seems to generate a lot of spurious increments. I implemented a new driver based on an elegant idea outlined [here](http://www.technoblogy.com/show?1YHJ). 41 | 42 | Lastly, the rotary controller knob press switch is connected to D33 which cannot be used in `setWatch` in the current ESP32 Espruino. Indeed it did not work as a digital input at all so I implemented a simple polled routine which used `analogRead` to detect the press event. 43 | 44 | ## TO BE DONE 45 | 46 | Installation instructions 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ats25/amradio.js: -------------------------------------------------------------------------------- 1 | eval(STOR.read("button.js")); 2 | eval(STOR.read("selector.js")); 3 | eval(STOR.read("bardisp.js")); 4 | 5 | var VOL=45; 6 | var STATE=0; //0 = VOLUME, 1 = FREQ, 2= BAND, 5 = NOTHING 7 | var FREQ = 198; 8 | var RSSI =0; 9 | var SNR =0; 10 | var BANDNAME ="LW"; 11 | var LOWBAND = 130; 12 | var HIGHBAND = 279; 13 | var STEP = 9; 14 | var BWindex = 1; 15 | var CAP =0; 16 | 17 | var buf = Graphics.createArrayBuffer(140,50,1,{msb:true}); 18 | 19 | var BANDS = (STOR.readJSON("bands.json")||[]).filter((e)=>{return e.mod=="AM";}); 20 | 21 | var BANDSEL = new Selector(BANDS,220,120); 22 | 23 | var BUTTONS=[ 24 | new Button("Scan+",80, 120, 60, 32, ()=>{scan(true,0);}), 25 | new Button("Scan-",80, 160, 60, 32, ()=>{scan(false,1);}), 26 | new Button("Mute",80, 200, 60, 32, (b)=>{RADIO.mute(b);}), 27 | new Button("Tune",10, 120, 60, 32, (b)=>{setSelector(b,1,4,5);}), 28 | new Button("Vol",10, 160, 60, 32, (b)=>{setSelector(b,0,3,5);}), 29 | new Button("Band",10, 200, 60, 32, (b)=>{setSelector(b,2,3,4);}), 30 | new Button("Step",150, 120, 60, 32, (b)=>{changeStep(b,6);}), 31 | new Button("BWid",150, 160, 60, 32, (b)=>{changeBW(b,7);}) 32 | ]; 33 | 34 | var VOLDISP = new BarDisp("VOL:",45,100,VOL); 35 | 36 | var stepindex= 2; 37 | const steps =[1,5,9,10]; 38 | function changeStep(b,n){ 39 | if (!b) return; 40 | stepindex = (stepindex+1)%4; 41 | STEP=steps[stepindex]; 42 | g.setColor(-1).setFont('6x8').setFontAlign(-1,-1).drawString("STEP: "+STEP+"KHz ",18,66,true); 43 | setTimeout(()=>{BUTTONS[n].reset();},200); 44 | } 45 | 46 | const bwidss =[6,4,3,2,1,1.8,2.5]; 47 | function changeBW(b,n){ 48 | if (!b) return; 49 | BWindex = (BWindex+1)%7; 50 | RADIO.setProp(0x3102,BWindex); 51 | g.setColor(-1).setFont('6x8').setFontAlign(-1,-1).drawString("Bwid: "+bwidss[BWindex].toFixed(1)+"KHz ",18,78,true); 52 | setTimeout(()=>{BUTTONS[n].reset();},200); 53 | } 54 | 55 | function drawFreq(){ 56 | buf.clear(); 57 | buf.setFont("Vector",42).setFontAlign(1,0).drawString((FREQ).toFixed(0),135,30); 58 | g.setColor(-1).drawImage(buf,120,20); 59 | } 60 | 61 | function drawBand() { 62 | g.setColor(Yellow); 63 | g.setFont('6x8').setFontAlign(-1,-1).drawString("BAND: "+BANDNAME+" ",18,30,true); 64 | g.drawString("MIN : "+LOWBAND+"KHz ",18,42,true); 65 | g.drawString("MAX : "+HIGHBAND+"KHz ",18,54,true); 66 | g.drawString("STEP: "+STEP+"KHz ",18,66,true); 67 | g.drawString("Bwid: "+bwidss[BWindex].toFixed(1)+"KHz ",18,78,true); 68 | } 69 | 70 | function drawSignal(){ 71 | g.setColor(Yellow); 72 | g.setFont('6x8').setFontAlign(-1,-1).drawString("RSSI: "+RSSI+" ",18,12,true); 73 | g.setFontAlign(-1,-1).drawString("SNR: "+SNR+" ",260,12,true); 74 | } 75 | 76 | function drawBat(){ 77 | var v = 7.15*analogRead(D35); 78 | g.setColor(Yellow).setFont('6x8').setFontAlign(-1,-1).drawString("BAT: "+v.toFixed(1)+"V",248,100,true); 79 | } 80 | 81 | function drawAM(){ 82 | g.setColor(Grey).fillRect(0,0,319,239); 83 | g.clearRect(10,10,310,110); 84 | g.setColor(-1).setFont("Vector",20).setFontAlign(-1,0).drawString("KHz",260,50); 85 | VOLDISP.draw(); 86 | BANDSEL.draw(true); 87 | for (var i=0;i{ 127 | if (!RADIO.endTune()) return; 128 | if (SCANNER) SCANNER=clearInterval(SCANNER); 129 | var r=RADIO.getTuneStatus(); 130 | FREQ=r.freq; SNR=r.snr; RSSI=r.rssi; 131 | drawFreq(); drawSignal(); 132 | BUTTONS[n].reset(); 133 | },100); 134 | } 135 | 136 | function initRADIO(){ 137 | RADIO.reset(); 138 | RADIO.powerAM(true); 139 | RADIO.setProp(0xFF00,0); //turn off debug see AN332 re noise 140 | RADIO.setProp(0x3102,BWindex); 141 | RADIO.volume(VOL); 142 | setBand(); 143 | } 144 | 145 | function setSelector(b,st,b1,b2){ 146 | if (!b) STATE=5; else STATE=st; 147 | BUTTONS[b1].reset(); 148 | BUTTONS[b2].reset(); 149 | BANDSEL.draw(true); 150 | if (STATE==2) setBand(); 151 | } 152 | 153 | function setControls(){ 154 | ROTARY.handler = (inc) => { 155 | if (STATE==1) 156 | {FREQ+=(inc*STEP); 157 | FREQ = FREQHIGHBAND?HIGHBAND:FREQ; 158 | setTune(FREQ); 159 | } else if(STATE==0) { 160 | VOL+=inc; 161 | VOL=VOL<0?0:VOL>63?63:VOL; 162 | VOLDISP.update(VOL); 163 | RADIO.volume(VOL); 164 | } else { 165 | BANDSEL.move(inc); 166 | if (BANDS.length!=0) setBand(); 167 | } 168 | }; 169 | ROTARY.on("change",ROTARY.handler); 170 | RADIO.signalQPoll = setInterval(()=>{ 171 | var r = RADIO.getSQ(); 172 | SNR=r.snr; RSSI=r.rssi; STEREO=r.stereo; 173 | drawSignal(); 174 | drawBat(); 175 | },500); 176 | TC.touchHandler = (p) => { 177 | for (var j = 0; j < BUTTONS.length; ++j) 178 | if (BUTTONS[j].isTouched(p)) { 179 | BUTTONS[j].toggle(); 180 | return; 181 | } 182 | }; 183 | TC.on("touch",TC.touchHandler); 184 | } 185 | 186 | function clearControls(){ 187 | clearD33(); 188 | if (ROTARY.handler){ 189 | ROTARY.removeListener("change",ROTARY.handler); 190 | delete ROTARY.handler; 191 | } 192 | if (RADIO.signalQPoll) RADIO.signalQPoll = clearInterval(RADIO.signalQPoll); 193 | if (TC.touchHandler){ 194 | TC.removeListener("touch",TC.touchHandler); 195 | delete TC.touchHandler; 196 | } 197 | } 198 | 199 | eval(STOR.read("keyboard.js")); 200 | 201 | var KBD; 202 | 203 | function toKBD() { 204 | SCREEN=1; 205 | clearControls(); 206 | g.setColor(Grey).fillRect(0,0,319,239); 207 | KBD = new Keyboard(LOWBAND,HIGHBAND,toRADIO,true).init(); 208 | KBD.enable(true).draw(); 209 | } 210 | 211 | function toRADIO() { 212 | SCREEN=0; 213 | KBD.enable(false); 214 | if (KBD.valid()) { 215 | setTune(KBD.freq()); 216 | } else { 217 | var f = KBD.freq(); 218 | var bi = BANDS.findIndex((e)=>{return f<=e.max && f>=e.min;}); 219 | if (bi>=0) { 220 | BANDSEL.pos=bi; 221 | BANDSEL.draw(true); 222 | setBand(f); 223 | } 224 | } 225 | delete KBD; 226 | setControls(); 227 | drawAM(); 228 | } 229 | 230 | var SCREEN = 0; 231 | 232 | TC.swipeHandler = (dir) => { 233 | if (dir == TC.DOWN && SCREEN == 0) { 234 | toKBD(); 235 | return; 236 | } 237 | if (dir == TC.UP && SCREEN == 1) { 238 | toRADIO(); 239 | return; 240 | } 241 | }; 242 | 243 | TC.on("swipe",TC.swipeHandler); 244 | 245 | initRADIO(); 246 | drawAM(); 247 | setControls(); 248 | 249 | -------------------------------------------------------------------------------- /ats25/app_buttons.js: -------------------------------------------------------------------------------- 1 | const Grey = g.toColor(0.6,0.6,0.6); 2 | const Green = g.toColor(0,1,0); 3 | const Yellow = g.toColor(1,1,0); 4 | const Blue = g.toColor(0,0,1); 5 | 6 | eval(STOR.read("button.js")); 7 | 8 | var VOL=35; 9 | var OLDVOL=0; 10 | var TUNEVOL=false; 11 | var FREQ = 9320; 12 | var RSSI =0; 13 | var SNR =0; 14 | var LOWBAND = 8750; 15 | var HIGHBAND = 10790; 16 | var SELECTED = -1; 17 | var buf = Graphics.createArrayBuffer(140,50,1,{msb:true}); 18 | 19 | var STATIONS = require("Storage").readJSON("stations.json")||[] 20 | STATIONS.sort((a,b)=>{ 21 | if(a.stationb.station)return 1; 23 | return 0; 24 | }); 25 | 26 | var NSTAT = STATIONS.length<=6 ? STATIONS.length : 6; 27 | 28 | var SBUTTON = []; 29 | for (var n = 0;n{scan(true,0);}), 34 | new Button("SCAN-",230, 150, 70, 32, ()=>{scan(false,1);}), 35 | new Button("MUTE",230, 190, 70, 32, (b)=>{RADIO.mute(b);}) 36 | ]; 37 | 38 | function drawFreq(){ 39 | buf.clear(); 40 | buf.setFont("Vector",48).setFontAlign(1,0).drawString((FREQ/100).toFixed(1),135,25); 41 | g.setColor(TUNEVOL?Green:-1).drawImage(buf,90,40); 42 | } 43 | 44 | function drawSignal(){ 45 | g.setColor(Yellow); 46 | g.setFont('6x8').setFontAlign(-1,-1).drawString("RSSI: "+RSSI+" ",28,22,true); 47 | g.setFontAlign(-1,-1).drawString("SNR: "+SNR+" ",250,22,true); 48 | } 49 | 50 | function drawBat(){ 51 | var v = 7.15*analogRead(D35); 52 | g.setColor(Yellow).setFont('6x8').setFontAlign(-1,-1).drawString("BAT: "+v.toFixed(1)+"V",238,90,true); 53 | } 54 | 55 | function drawVolume(){ 56 | var v = VOL; 57 | if (v>=OLDVOL) g.setColor(TUNEVOL?Grey:Green).fillRect(55,91,55+v,97); 58 | if (v=0) {SBUTTON[SELECTED].reset(); SELECTED=-1;} 92 | if(BUTTONS[(n+1)%2].press){BUTTONS[(n+1)%2].reset();} 93 | RADIO.seek(up,false); 94 | SCANNER=setInterval(()=>{ 95 | if (!RADIO.endTune()) return; 96 | if (SCANNER) SCANNER=clearInterval(SCANNER); 97 | var r=RADIO.getTuneStatus(); 98 | FREQ=r.freq; SNR=r.snr; RSSI=r.rssi; 99 | drawFreq(); drawSignal(); 100 | BUTTONS[n].reset(); 101 | },100); 102 | } 103 | 104 | function initRADIO(){ 105 | RADIO.reset(); 106 | RADIO.powerFM(true); 107 | RADIO.setProp(0x12,0xFF00,0); //turn off debug see AN332 re moise 108 | RADIO.volume(VOL); 109 | } 110 | 111 | function setControls(){ 112 | watchD33(()=>{ 113 | TUNEVOL=!TUNEVOL; 114 | if(TUNEVOL && SELECTED>=0) { 115 | SBUTTON[SELECTED].reset(); 116 | SELECTED=-1; 117 | } 118 | drawVolume(); 119 | drawFreq(); 120 | }); 121 | ROTARY.handler = (inc) => { 122 | if (TUNEVOL) 123 | {FREQ+=(inc*10); 124 | FREQ = FREQHIGHBAND?HIGHBAND:FREQ; 125 | setTune(FREQ); 126 | } else { 127 | VOL+=inc; 128 | VOL=VOL<0?0:VOL>63?63:VOL; 129 | drawVolume(); 130 | RADIO.volume(VOL); 131 | } 132 | }; 133 | ROTARY.on("change",ROTARY.handler); 134 | RADIO.signalQPoll = setInterval(()=>{ 135 | var r = RADIO.getSQ(); 136 | SNR=r.snr; RSSI=r.rssi; 137 | drawSignal(); 138 | drawBat(); 139 | },500); 140 | TC.touchHandler = (p) => { 141 | if (!TUNEVOL) 142 | for (var i = 0; i < NSTAT; ++i) 143 | if (SBUTTON[i].isTouched(p)) { 144 | if (SELECTED >= 0) 145 | SBUTTON[SELECTED].reset(); 146 | SBUTTON[i].set(); 147 | setTune(STATIONS[i].freq); 148 | SELECTED = i; 149 | return; 150 | } 151 | for (var j = 0; j < 3; ++j) 152 | if (BUTTONS[j].isTouched(p)) { 153 | BUTTONS[j].toggle(); 154 | return; 155 | } 156 | }; 157 | TC.on("touch",TC.touchHandler); 158 | } 159 | 160 | function clearControls(){ 161 | clearD33(); 162 | if (ROTARY.handler){ 163 | ROTARY.removeListener("change",ROTARY.handler); 164 | delete ROTARY.handler; 165 | } 166 | if (RADIO.signalQPoll) RADIO.signalQPoll = clearInterval(RADIO.signalQPoll); 167 | if (TC.touchHandler){ 168 | TC.removeListener("touch",TC.touchHandler); 169 | delete TC.touchHandler; 170 | } 171 | } 172 | 173 | eval(STOR.read("keyboard.js")); 174 | 175 | initRADIO(); 176 | drawFM(); 177 | setControls(); 178 | 179 | -------------------------------------------------------------------------------- /ats25/bardisp.js: -------------------------------------------------------------------------------- 1 | class BarDisp { 2 | constructor(str,x,y,v){ 3 | this.x=x; this.y=y; 4 | this.str=str; 5 | this.v=v; 6 | this.old=0; 7 | } 8 | 9 | update(v){ 10 | if (v>=this.old) g.setColor(Green).fillRect(this.x,this.y+1,this.x+v,this.y+7); 11 | if (vthis.x1 && p.xthis.y1 && p.y{ 22 | this.reset(); 23 | this.fn(this.str); 24 | },200); 25 | } 26 | toggle(){ 27 | this.press=!this.press; 28 | this.draw(); 29 | this.fn(this.press); 30 | } 31 | set(){ 32 | this.press=true; 33 | this.draw(); 34 | } 35 | reset(){ 36 | this.press=false; 37 | this.draw(); 38 | } 39 | actOnTouch(p){ 40 | if (this.isTouched(p)) { 41 | this.action(p); 42 | return true; 43 | } else return false; 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ats25/chooser.js: -------------------------------------------------------------------------------- 1 | const Grey = g.toColor(0.6,0.6,0.6); 2 | const Green = g.toColor(0,1,0); 3 | const Yellow = g.toColor(1,1,0); 4 | const Blue = g.toColor(0,0,1); 5 | 6 | g.clear(); 7 | g.drawImage(STOR.read("radio.png"),60,20); 8 | 9 | eval(STOR.read("button.js")); 10 | var FMBUTTON = new Button("FM",20,160,80,40,()=>{load("fmradio.js");},28); 11 | var AMBUTTON = new Button("AM",120,160,80,40,()=>{load("amradio.js");},28); 12 | var SSBUTTON = new Button("SSB",220,160,80,40,()=>{load("ssbradio.js");},28); 13 | 14 | FMBUTTON.draw(); 15 | AMBUTTON.draw(); 16 | SSBUTTON.draw(); 17 | g.setColor(-1).setFont("6x8",1).setFontAlign(0,-1).drawString("Long touch to return to this screen.",160,220); 18 | 19 | TC.on("touch",(p)=>{FMBUTTON.actOnTouch(p); 20 | AMBUTTON.actOnTouch(p); 21 | SSBUTTON.actOnTouch(p); 22 | }); 23 | -------------------------------------------------------------------------------- /ats25/fmradio.js: -------------------------------------------------------------------------------- 1 | eval(STOR.read("button.js")); 2 | eval(STOR.read("selector.js")); 3 | eval(STOR.read("bardisp.js")); 4 | 5 | var VOL=45; 6 | var STATE=0; //0 = VOLUME, 1 = FREQ, 2= STATION, 5 = NOTHING 7 | var FREQ = 9320; 8 | var RSSI =0; 9 | var SNR =0; 10 | var STEREO=0; 11 | var LOWBAND = 8750; 12 | var HIGHBAND = 10790; 13 | var buf = Graphics.createArrayBuffer(140,50,1,{msb:true}); 14 | eval(STOR.read("rds.js")); 15 | 16 | var STATIONS = STOR.readJSON("stations.json")||[] 17 | STATIONS.sort((a,b)=>{ 18 | if(a.stationb.station)return 1; 20 | return 0; 21 | }); 22 | 23 | var STATSEL = new Selector(STATIONS,220,120); 24 | 25 | var BUTTONS=[ 26 | new Button("Scan+",80, 120, 60, 32, ()=>{scan(true,0);}), 27 | new Button("Scan-",80, 160, 60, 32, ()=>{scan(false,1);}), 28 | new Button("Mute",80, 200, 60, 32, (b)=>{RADIO.mute(b);}), 29 | new Button("Tune",10, 120, 60, 32, (b)=>{setSelector(b,1,4,5);}), 30 | new Button("Vol",10, 160, 60, 32, (b)=>{setSelector(b,0,3,5);}), 31 | new Button("Pre",10, 200, 60, 32, (b)=>{setSelector(b,2,3,4)}), 32 | new Button("RDS",150, 120, 60, 32, (b)=>{if (b) rdsStart(); else rdsStop();}), 33 | new Button("Add",150,160,60,32,(b)=>{addStation(b)}), 34 | new Button("Del",150,200,60,32,(b)=>{delStation(b)}) 35 | ]; 36 | 37 | function addStation(b){ 38 | if(b && SEGMENTS==15) { 39 | STATSEL.add(getname(),FREQ); 40 | STATSEL.draw(1); 41 | STOR.writeJSON("stations.json",STATIONS); 42 | } 43 | BUTTONS[7].reset(); 44 | } 45 | 46 | function delStation(b){ 47 | if(b && BUTTONS[5].press) { 48 | STATSEL.del(); 49 | STATSEL.draw(1); 50 | if (STATIONS.length!=0) setTune(STATSEL.freq()); 51 | STOR.writeJSON("stations.json",STATIONS); 52 | } 53 | BUTTONS[8].reset(); 54 | } 55 | 56 | var VOLDISP = new BarDisp("VOL:",45,100,VOL); 57 | 58 | function drawFreq(){ 59 | buf.clear(); 60 | buf.setFont("Vector",48).setFontAlign(1,0).drawString((FREQ/100).toFixed(1),135,30); 61 | g.setColor(-1).drawImage(buf,110,20); 62 | } 63 | 64 | function drawSignal(){ 65 | g.setColor(Yellow); 66 | g.setFont('6x8').setFontAlign(-1,-1).drawString("RSSI: "+RSSI+" ",18,12,true); 67 | g.setFontAlign(0,-1).drawString(STEREO==1?"Stereo":" ",160,12,true); 68 | g.setFontAlign(-1,-1).drawString("SNR: "+SNR+" ",260,12,true); 69 | } 70 | 71 | function drawBat(){ 72 | var v = 7.15*analogRead(D35); 73 | g.setColor(Yellow).setFont('6x8').setFontAlign(-1,-1).drawString("BAT: "+v.toFixed(1)+"V",248,100,true); 74 | } 75 | 76 | function drawFM(){ 77 | g.setColor(Grey).fillRect(0,0,319,239); 78 | g.clearRect(10,10,310,110); 79 | g.setColor(-1).setFont("Vector",20).setFontAlign(-1,0).drawString("MHz",250,50); 80 | VOLDISP.draw(); 81 | STATSEL.draw(); 82 | for (var i=0;i{ 106 | if (!RADIO.endTune()) return; 107 | if (SCANNER) SCANNER=clearInterval(SCANNER); 108 | var r=RADIO.getTuneStatus(); 109 | FREQ=r.freq; SNR=r.snr; RSSI=r.rssi; 110 | drawFreq(); drawSignal(); 111 | BUTTONS[n].reset(); 112 | },100); 113 | } 114 | 115 | function initRADIO(){ 116 | RADIO.reset(); 117 | RADIO.powerFM(true); 118 | RADIO.setProp(0xFF00,0); //turn off debug see AN332 re noise 119 | //RADIO.setProp(0x1800,127); // set to blend set to mono 120 | //RADIO.setProp(0x1801,127); // mono blend threshold - force mono 121 | RADIO.volume(VOL); 122 | } 123 | 124 | function setSelector(b,st,b1,b2){ 125 | if (b) STATE=st; else return; 126 | BUTTONS[b1].reset(); 127 | BUTTONS[b2].reset(); 128 | STATSEL.draw(STATE==2); 129 | if (STATE==2 && STATIONS.length!=0) setTune(STATSEL.freq()); else drawFreq(); 130 | } 131 | 132 | function setControls(){ 133 | ROTARY.handler = (inc) => { 134 | if (STATE==1) 135 | {FREQ+=(inc*10); 136 | FREQ = FREQHIGHBAND?HIGHBAND:FREQ; 137 | setTune(FREQ); 138 | } else if(STATE==0) { 139 | VOL+=inc; 140 | VOL=VOL<0?0:VOL>63?63:VOL; 141 | VOLDISP.update(VOL); 142 | RADIO.volume(VOL); 143 | } else if (STATE==2) { 144 | STATSEL.move(inc); 145 | if (STATIONS.length!=0) setTune(STATSEL.freq()); 146 | } 147 | }; 148 | ROTARY.on("change",ROTARY.handler); 149 | RADIO.signalQPoll = setInterval(()=>{ 150 | var r = RADIO.getSQ(); 151 | SNR=r.snr; RSSI=r.rssi; STEREO=r.stereo; 152 | drawSignal(); 153 | drawBat(); 154 | },500); 155 | TC.touchHandler = (p) => { 156 | for (var j = 0; j < BUTTONS.length; ++j) 157 | if (BUTTONS[j].isTouched(p)) { 158 | BUTTONS[j].toggle(); 159 | return; 160 | } 161 | }; 162 | TC.on("touch",TC.touchHandler); 163 | if (BUTTONS[6].press) rdsStart(); 164 | } 165 | 166 | function clearControls(){ 167 | clearD33(); 168 | if (ROTARY.handler){ 169 | ROTARY.removeListener("change",ROTARY.handler); 170 | delete ROTARY.handler; 171 | } 172 | if (RADIO.signalQPoll) RADIO.signalQPoll = clearInterval(RADIO.signalQPoll); 173 | if (TC.touchHandler){ 174 | TC.removeListener("touch",TC.touchHandler); 175 | delete TC.touchHandler; 176 | } 177 | rdsStop(); 178 | } 179 | 180 | eval(STOR.read("keyboard.js")); 181 | 182 | function toKBD() { 183 | SCREEN=1; 184 | clearControls(); 185 | g.setColor(Grey).fillRect(0,0,319,239); 186 | KBD.enable(true).draw(); 187 | } 188 | 189 | function toRADIO() { 190 | SCREEN=0; 191 | KBD.enable(false); 192 | if (KBD.valid()) { 193 | setTune(KBD.freq()); 194 | SELECTED=-1; 195 | } 196 | setControls(); 197 | drawFM(); 198 | } 199 | 200 | var KBD = new Keyboard(LOWBAND,HIGHBAND,toRADIO,false).init(); 201 | 202 | var SCREEN = 0; 203 | 204 | TC.swipeHandler = (dir) => { 205 | if (dir == TC.DOWN && SCREEN == 0) { 206 | toKBD(); 207 | return; 208 | } 209 | if (dir == TC.UP && SCREEN == 1) { 210 | toRADIO(); 211 | return; 212 | } 213 | }; 214 | 215 | TC.on("swipe",TC.swipeHandler); 216 | 217 | initRADIO(); 218 | drawFM(); 219 | setControls(); 220 | 221 | -------------------------------------------------------------------------------- /ats25/ili9342.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Gordon Williams, Pur3 Ltd. See the file LICENSE for copying permission. 3 | 4 | Updated for use in ESP32 using lcd_spi_unbuf by Jeff Magee 5 | */ 6 | 7 | function ILI9342() { 8 | var LCD_WIDTH = 320; 9 | var LCD_HEIGHT = 240; 10 | var XOFF = 0; 11 | var YOFF = 0; 12 | var cmd = lcd_spi_unbuf.command; 13 | 14 | function dispinit(rst,fn) { 15 | function delayms(d) {var t = getTime()+d/1000; while(getTime(){g.clear().setFont("Vector",24).drawString("ESP32 Espruino",40,100);}); 62 | return g; 63 | } 64 | 65 | //var spi = new SPI(); 66 | //SPI1.setup({sck:D18, mosi:D23, baud: 20000000}); 67 | 68 | return connect({spi:SPI1, dc:D2, cs:D15, rst:D4}); 69 | } 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /ats25/keyboard.js: -------------------------------------------------------------------------------- 1 | class Keyboard { 2 | constructor(low,high,fn,khz){ 3 | this.low = low; 4 | this.high = high; 5 | this.buf = Graphics.createArrayBuffer(160,50,1,{msb:true}); 6 | this.keys = []; 7 | this.khz = khz; 8 | this.ACC=0; 9 | this.VALID=false; 10 | this.fn = fn; 11 | } 12 | 13 | display() { 14 | this.buf.clear(); 15 | this.buf.setFont("Vector",32).setFontAlign(1,0); 16 | if (this.khz) this.buf.drawString((this.freq()).toFixed(0),135,25); 17 | else this.buf.drawString((this.freq()/100).toFixed(1),135,25); 18 | g.setColor(this.valid()?Green:-1).drawImage(this.buf,80,5); 19 | } 20 | 21 | freq() { 22 | return this.khz?this.ACC:this.ACC*10; 23 | } 24 | 25 | valid() { 26 | var f = this.freq(); 27 | return (f<=this.high&&f>=this.low); 28 | } 29 | 30 | update(s){ 31 | if (s!="<") 32 | this.ACC=this.ACC*10+s; 33 | else 34 | this.ACC=Math.floor(this.ACC/10); 35 | this.display(); 36 | } 37 | 38 | init(){ 39 | var keyfn = (s) => {this.update(s);}; 40 | for (var j =0;j<3;j++) 41 | for (var i =0;i<3;i++) 42 | this.keys[j*3+i+1] = new Button(j*3+i+1,80+55*i,60+j*41,50,36,keyfn,28); 43 | this.keys[0] = new Button(0,80,183,75,36,keyfn,28); 44 | this.keys[10] = new Button("<",165,183,75,36,keyfn,28); 45 | this.keys[11] = new Button("Enter",245,183,70,36,this.fn); 46 | return this; 47 | } 48 | 49 | enable(b) { 50 | if (b) { 51 | this.ACC = 0; 52 | TC.touchHandler = (p)=> { 53 | for (var i =0;i{ 24 | var pressed = (analogRead(D33)< 0.2); 25 | if (fn && pressed && !global.D33STATE) fn(); 26 | global.D33STATE=pressed; 27 | },200); 28 | } 29 | 30 | function clearD33(){ if (global.D33POLL) global.D33POLL = clearInterval(global.D33POLL);} 31 | 32 | TC.on("longtouch",()=>{load("chooser.js");}); 33 | 34 | -------------------------------------------------------------------------------- /ats25/radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/ats25/radio.png -------------------------------------------------------------------------------- /ats25/rds.js: -------------------------------------------------------------------------------- 1 | var STATNAME = new Uint8Array(8); 2 | var SEGMENTS = 0; 3 | var RDSMSG = new Uint8Array(64); 4 | var AB = 0; 5 | 6 | function clearMsg(){ 7 | for(var i=0;i{return e==0 || e==42}); 12 | end = end<0?63:end; 13 | if (end <8) return null; 14 | return end<63 ? E.toString(RDSMSG.subarray(0,end)): E.toString(RDSMSG); 15 | } 16 | 17 | function rdsInit(){ 18 | RADIO.setProp(0x1500,0x0001); // enable receive interrupt 19 | RADIO.setProp(0x1501,0x0004); // set fifo theshold for interrupt 20 | RADIO.setProp(0x1502,0xAA01); // set error correction and enable 21 | } 22 | 23 | function getname(){ 24 | if (SEGMENTS==15) { 25 | return E.toString(STATNAME); 26 | } else return null; 27 | } 28 | 29 | function drawStation(s){ 30 | if (s) g.setColor(-1).setFont("6x8",2).setFontAlign(-1,-1).drawString(s,20,40,true); 31 | } 32 | 33 | var rdsbuf = Graphics.createArrayBuffer(280,20,1,{msb:true}); 34 | rdsbuf.setFont("6x8",1).setFontAlign(-1,-1); 35 | 36 | function drawMsg(msg){ 37 | rdsbuf.clear(); 38 | if (msg) msg= rdsbuf.drawString(rdsbuf.wrapString(msg, rdsbuf.getWidth()).join("\n"),0,0); 39 | g.setColor(-1).drawImage(rdsbuf,20,75); 40 | } 41 | 42 | function decode(m){ 43 | function cl(c){ c=c&0x7F; return c<32?42:c;} 44 | var g = (m[6]&0xF0)>>4; 45 | var b0 = (m[6]&0x08)>>3; 46 | var o = g==0?m[7]&0x03:m[7]&0x0f; 47 | if (g==2) { 48 | var ab = (m[7]&0x10)>>4; 49 | if (ab!=AB) {AB=ab; clearMsg();} 50 | } 51 | if (g==0){ 52 | STATNAME[2*o]=cl(m[10]); 53 | STATNAME[2*o+1]=cl(m[11]); 54 | SEGMENTS=SEGMENTS|(1<{ 71 | if(RADIO.hasRDS()){ 72 | while (true) { 73 | var dd = RADIO.getRDS(); 74 | if (dd[3]==0) break; 75 | decode(dd); 76 | } 77 | } 78 | var name = getname(); 79 | drawStation(name); 80 | var msg = getMsg(); 81 | drawMsg(msg); 82 | },1000); 83 | } 84 | 85 | function rdsClear(){ 86 | SEGMENTS=0; 87 | clearMsg(); 88 | drawMsg(null); 89 | drawStation(" "); 90 | } 91 | 92 | function rdsStop(){ 93 | rdsClear(); 94 | if (global.RDSPOLL) global.RDSPOLL=clearInterval(global.RDSPOLL); 95 | } 96 | 97 | 98 | -------------------------------------------------------------------------------- /ats25/selector.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class Selector { 5 | constructor(list,x,y){ 6 | this.list=list; 7 | this.px=x; 8 | this.py=y; 9 | this.pos=0; 10 | this.buf = Graphics.createArrayBuffer(90,110,2,{msb:true}); 11 | this.pal = new Uint16Array([0,-1,Grey,Green]); 12 | this.buf.setFontAlign(0,0).setColor(2).fillRect(0,0,89,109);; 13 | } 14 | 15 | mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 16 | 17 | draw(b) { 18 | this.buf.setColor(0).fillRect({x:0,y:0,x1:89,y2:109,r:10}); 19 | function mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 20 | var len = this.list.length; 21 | if (len!=0){ 22 | var bound = len>=5?2:len>=3?1:0; 23 | for (var i =-bound;i<=bound;i++) 24 | this.buf.setColor(i==0&&b?1:2).setFont("Vector",18-(i>=0?2*i:2*-i)).drawString(this.list[mod(this.pos+i,len)].name,45,55+20*i); 25 | } 26 | g.drawImage({width:90,height:110,bpp:2,palette:this.pal,buffer:this.buf.buffer},this.px,this.py); 27 | } 28 | 29 | move(inc){ 30 | if (this.list.length==0) return; 31 | this.pos=this.mod(this.pos+inc,this.list.length); 32 | this.draw(true); 33 | } 34 | 35 | freq(){ 36 | return this.list[this.pos].freq; 37 | } 38 | 39 | selected(){ 40 | return this.list[this.pos]; 41 | } 42 | 43 | add(newname,f){ 44 | if (this.list.length!=0 && newname==this.list[this.pos].name) return; 45 | this.list.splice(this.pos,0,{name:newname,freq:f}); 46 | } 47 | 48 | del(){ 49 | if(this.list.length==0) return; 50 | this.list.splice(this.pos,1); 51 | if (this.pos==this.list.length) --this.pos; 52 | if (this.pos<0) this.pos=0; 53 | } 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /ats25/si4735.js: -------------------------------------------------------------------------------- 1 | //var RADIOI2C = new I2C(); 2 | // var ret = 0; 3 | // while (ret==0) ret = RADIO.read(1)[0]; 4 | // if (ret!=128) return false; 5 | I2C1.setup({scl:D22,sda:D21,bitrate:200000}); 6 | 7 | var RADIO = { 8 | isFM:null, 9 | delayms:(d) => { 10 | var t = getTime()+d/1000; while(getTime() { 13 | I2C1.writeTo(0x63,d); 14 | }, 15 | read:(n) => { 16 | return I2C1.readFrom(0x63,n); 17 | }, 18 | reset:() => { 19 | D12.set(); 20 | digitalPulse(D12,0,10); 21 | RADIO.delayms(10); 22 | 23 | }, 24 | waitCTS:(d)=>{ 25 | for(var i=0;i<100;i++) if(RADIO.read(1)[0]&0x80) return true; 26 | return false; 27 | }, 28 | powerFM:(b)=>{ 29 | if (b) { 30 | RADIO.isFM=true; 31 | RADIO.write([0x01,0x90,0x05]); //FM analogue 32 | } else { 33 | RADIO.write(0x11); 34 | } 35 | RADIO.delayms(500); //stabilisation delay 36 | return RADIO.waitCTS(); 37 | }, 38 | powerAM:(b)=>{ 39 | if (b) { 40 | RADIO.isFM=false; 41 | RADIO.write([0x01,0x91,0x05]); //AM analogue 42 | } else { 43 | RADIO.write(0x11); 44 | } 45 | RADIO.delayms(500); //stabilisation delay 46 | return RADIO.waitCTS(); 47 | }, 48 | powerSSB:(b)=>{ 49 | if (b) { 50 | RADIO.isFM=false; 51 | RADIO.write([0x01,0xB1,0x05]); //AM patch analogue 52 | if (!RADIO.waitCTS()) return false; 53 | var nb = SSB.lenPatch(); 54 | var next = 0; 55 | while(next{ 67 | if (!RADIO.waitCTS()) return; 68 | var cm = new Uint8Array(5); 69 | cm[0] = 0x20; 70 | cm[1] = 0x00; 71 | cm[2] = f>>8; 72 | cm[3] = f & 0xFF; 73 | cm[4] = 0; 74 | RADIO.write(cm); 75 | }, 76 | tuneAM:(f,sw)=>{ 77 | if (!RADIO.waitCTS()) return; 78 | var cm = new Uint8Array(6); 79 | cm[0] = 0x40; 80 | cm[1] = 0x00; 81 | cm[2] = f>>8; 82 | cm[3] = f & 0xFF; 83 | cm[4] = 0; 84 | cm[5] = sw?1:0; 85 | RADIO.write(cm); 86 | }, 87 | tuneSSB:(f,sw,usb)=>{ 88 | if (!RADIO.waitCTS()) return; 89 | var cm = new Uint8Array(6); 90 | cm[0] = 0x40; 91 | cm[1] = usb?0x80:0x40; 92 | cm[2] = f>>8; 93 | cm[3] = f & 0xFF; 94 | cm[4] = 0; 95 | cm[5] = sw?1:0; 96 | RADIO.write(cm); 97 | }, 98 | endTune:()=>{ 99 | if (!RADIO.waitCTS()) return; 100 | RADIO.write(0x14); 101 | var s = RADIO.read(1)[0]; 102 | return (s&1)>0; 103 | }, 104 | getTuneStatus:()=>{ 105 | if (!RADIO.waitCTS()) return; 106 | var cm = new Uint8Array(2); 107 | cm[0] = RADIO.isFM?0x22:0x42; 108 | cm[1] = 0x01; 109 | RADIO.write(cm); 110 | var res = RADIO.read(8); 111 | return {status:res[0],valid:res[1],freq:(res[2]<<8)+res[3],rssi:res[4],snr:res[5]}; 112 | }, 113 | seek:(up,wrap)=>{ 114 | if (!RADIO.waitCTS()) return; 115 | var cm = new Uint8Array(2); 116 | cm[0] = RADIO.isFM?0x21:0x41; 117 | var arg = 0; 118 | if (up) arg = arg|0x08; 119 | if (wrap) arg = arg | 0x04; 120 | cm[1] = arg; 121 | RADIO.write(cm); 122 | }, 123 | getSQ:()=>{ 124 | if (!RADIO.waitCTS()) return; 125 | var cm = new Uint8Array(2); 126 | cm[0] = RADIO.isFM?0x23:0x43; 127 | cm[1] = 0x00; 128 | RADIO.write(cm); 129 | var res = RADIO.read(RADIO.isFM?8:6); 130 | return {status:res[0],valid:res[2],stereo:res[3]>>7,rssi:res[4],snr:res[5]}; 131 | }, 132 | getAGC:()=>{ 133 | if (!RADIO.waitCTS()) return; 134 | var cm = new Uint8Array(1); 135 | cm[0] = 0x47; 136 | RADIO.write(cm); 137 | var res = RADIO.read(3); 138 | return res; 139 | }, 140 | setAGC:(enable,val)=>{ 141 | if (!RADIO.waitCTS()) return; 142 | var cm = new Uint8Array(3); 143 | cm[0] = 0x48; 144 | cm[1] = enable?0:1; 145 | cm[2] = val; 146 | RADIO.write(cm); 147 | }, 148 | getRDS:()=>{ 149 | if (!RADIO.waitCTS()) return; 150 | var cm = new Uint8Array(2); 151 | cm[0] = 0x24; 152 | cm[1] = 0x01; 153 | RADIO.write(cm); 154 | var res = RADIO.read(13); 155 | return res; 156 | }, 157 | hasRDS:()=>{ 158 | if (!RADIO.waitCTS()) return; 159 | RADIO.write(0x14); 160 | var s = RADIO.read(1)[0]; 161 | return (s&4)>0; 162 | }, 163 | setProp:(id,v)=>{ 164 | if (!RADIO.waitCTS()) return; 165 | var cm = new Uint8Array(6); 166 | cm[0] = 0x12; 167 | cm[1] = 0; 168 | cm[2] = id>>8; 169 | cm[3] = id&0xff; 170 | cm[4] = v>>8; 171 | cm[5] = v&0xff; 172 | RADIO.write(cm); 173 | return RADIO.read(1)[0]; 174 | }, 175 | getProp:(id)=>{ 176 | if (!RADIO.waitCTS()) return; 177 | var cm = new Uint8Array(4); 178 | cm[0] = 0x13; 179 | cm[1] = 0; 180 | cm[2] = id>>8; 181 | cm[3] = id&0xff; 182 | RADIO.write(cm); 183 | var res = RADIO.read(4); 184 | return {status:res[0],value:(res[2]<<8)+res[3]}; 185 | }, 186 | volume:(v)=>{RADIO.setProp(0x4000,v<0?0:v>63?63:v);}, 187 | mute:(b)=>{RADIO.setProp(0x4001,b?3:0);} 188 | }; -------------------------------------------------------------------------------- /ats25/ssbradio.js: -------------------------------------------------------------------------------- 1 | eval(STOR.read("button.js")); 2 | eval(STOR.read("selector.js")); 3 | eval(STOR.read("bardisp.js")); 4 | 5 | Graphics.prototype.setFontDMMono = function(scale) { 6 | // Actual height 27 (26 - 0) 7 | this.setFontCustom( 8 | atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAB8AAAAfAAAAHwAAAB8AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAOAAAAfgAAAf4AAA/8AAA/4AAB/4AAB/4AAD/wAAD/wAAD/gAAD/gAAA/AAAAPAAAACAAAAAAAAAAAAAAAAAAAAAAf/gAAf/+AAP//wAH//+AB8APgA+Af8APAf/ADgf5wA4/4cAP/4PAD/wDwA/wB8AH4B+AA///AAH//gAA//wAAD/wAAAAAAAAAAAAAAAAAAOAAcADgAHAB4ABwAeAAcAHAAHAD///wA///8AP///AD///wAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAAAAAAAAAAAAAAAAAMAB4APAA+ADwAfgB8AHwA/ADwAfwA8AP8AOAD3ADgB5wA8B8cAPA+HAD9/BwAf/gcAD/wHAAfwBwAAAAcAAAAAAAAAAAAAAAAAAABgAAcA+AAPAPwAHwB+AB8AHgA8AA8APDgPADg4BwA4OAcAODgHADx8DwA//A8AH//+AB/P/gAPh/wAAAPwAAAAAAAAAAAAAAAAAAAA4AAAA+AAAAfgAAAf4AAAP+AAAPzgAAH44AAD4OAAD8DgAB8A4AA+AOAAP///AD///wA///8AAADgAAAA4AAAAOAAAAAAAAAAAAAAAGAAAfx4AD/8fAA//H4AP/g+ADhwDwA48A8AOOAHADjgBwA44AcAOPAPADjwHwA4fD4AOH/8ADg/+AAAD/AAAAAAAAAAAAAAAAAAAAQAAAD/4AAD//gAD//8AB/f/gAePB4APDgPADx4DwA4cAcAOHAHADhwBwA8eA8APHwfAB4//gAfH/wADw/4AAMD4AAAAAAAAAAAAAAAAAA4AAAAOAAAADgAAAA4AAEAOAAPADgAPwA4AP8AOAP+ADgf8AA4f8AAOf8AAD/4AAA/4AAAP4AAAD4AAAAAAAAAAAAAAAAAAAAAAfAAB8P4AA/n/AAf//4AP/4fADw+DwA4PAcAOBwHADgcBwA4HAcAPD4PAD5+DwAf//4AD/f+AAfj/AAAAfAAAAAAAAAAAAAAAAAAAfAwAAf4OAAP/DwAH/4+AD4fHwA8Dw8AOAcHADgHBwA4BwcAOAcPADwPDwA+Hh4AH/z+AA///AAH//AAAf/gAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgHgAB8B8AAfAfAAHwHwAB8B8AAEAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='), 9 | 46, 10 | 19, 11 | 32+(scale<<8)+(1<<16) 12 | ); 13 | return this; 14 | } 15 | 16 | 17 | var VOL=35; 18 | var STATE=0; //0 = VOLUME, 1 = FREQ, 2= BAND, 5 = DO Nothing 19 | var TUNEORSTEP = false; 20 | var TUNEDFREQ=0; //Hz 21 | var FREQ = 0; //x 10000 Hz 22 | var BFO = 0; 23 | var RSSI =0; 24 | var SNR =0; 25 | var BANDNAME ="LW"; 26 | var LOWBAND = 4700; 27 | var HIGHBAND = 5500; 28 | var MOD="USB"; 29 | var SSB_MODE=0x9001; //AFC disable, AVC enable, bandpass&cutoff, 2.2Khz BW 30 | var STEP = 1000; 31 | var BWindex = 1; 32 | var CAP =1; 33 | 34 | var BANDS = (STOR.readJSON("bands.json")||[]).filter((e)=>{return e.mod!="AM";}); 35 | 36 | var BANDSEL = new Selector(BANDS,220,120); 37 | 38 | var VOLDISP = new BarDisp("VOL:",45,100,VOL); 39 | 40 | var BUTTONS=[ 41 | new Button("MOD",80, 120, 60, 32, (b)=>{changeSideBand(b,0);}), 42 | new Button("BWid",80, 160, 60, 32, (b)=>{changeBW(b,1);}), 43 | new Button("Mute",80, 200, 60, 32, (b)=>{RADIO.mute(b);}), 44 | new Button("Tune",10, 120, 60, 32, (b)=>{setSelector(b,1,4,5);}), 45 | new Button("Vol",10, 160, 60, 32, (b)=>{setSelector(b,0,3,5);}), 46 | new Button("Band",10, 200, 60, 32, (b)=>{setSelector(b,2,3,4);}), 47 | ]; 48 | 49 | 50 | function setModulation(){ 51 | if (MOD=="SYN") SSB_MODE = SSB_MODE & 0x7fff; 52 | else SSB_MODE = SSB_MODE | 0x8000; 53 | RADIO.setProp(0x0101,SSB_MODE); 54 | } 55 | 56 | function changeSideBand(b,n){ 57 | if (!b) return; 58 | MOD = (MOD=="USB")?"LSB":(MOD=="LSB")?"SYN":(MOD=="SYN")?"USB":"USB"; 59 | setModulation(); 60 | g.setFont('6x8').setFontAlign(0,-1).drawString(MOD,160,12,true); 61 | setTune(TUNEDFREQ,true); //force tune to change side band 62 | setTimeout(()=>{BUTTONS[n].reset();},200); 63 | } 64 | 65 | var stepindex= 2; 66 | const steps =[10,100,1000,10000]; 67 | 68 | function changeStep(n){ 69 | function mod(a,n) {return a>=n?a-n:a<0?a+n:a;} 70 | stepindex+=n; 71 | stepindex = mod(stepindex,4); 72 | STEP=steps[stepindex]; 73 | g.setColor(-1).setFont('6x8').setFontAlign(-1,-1).drawString("STEP: "+STEP+"Hz ",18,78,true); 74 | g.clearRect(160,70,265,74); 75 | var x = 242 - (stepindex<2?stepindex:stepindex+1)*18; 76 | g.setColor(TUNEORSTEP?-1:Grey).fillRect(x,70,x+16,74); 77 | } 78 | 79 | const bwidss =[1.2,2.2,3,4,0.5,1]; 80 | function changeBW(b,n){ 81 | if (!b) return; 82 | BWindex = (BWindex+1)%6; 83 | var pat = bwidss[BWindex]<2.5 ? BWindex : 0x10 | BWindex; 84 | SSB_MODE = (SSB_MODE & 0xFF00) | pat; 85 | RADIO.setProp(0x0101,SSB_MODE); 86 | g.setColor(-1).setFont('6x8').setFontAlign(-1,-1).drawString("Bwid: "+bwidss[BWindex].toFixed(1)+"KHz ",18,42,true); 87 | setTimeout(()=>{BUTTONS[n].reset();},200); 88 | } 89 | 90 | var buf = Graphics.createArrayBuffer(155,40,1,{msb:true}); 91 | function drawFreq(){ 92 | buf.clear(); 93 | buf.setFontAlign(1,0).setFontDMMono().drawString((TUNEDFREQ/1000).toFixed(2),153,25); 94 | g.setColor(-1).drawImage(buf,110,30); 95 | } 96 | 97 | function drawBand() { 98 | g.setColor(Yellow); 99 | g.setFont('6x8').setFontAlign(-1,-1).drawString("BAND: "+BANDNAME+" ",18,30,true); 100 | g.drawString("Bwid: "+bwidss[BWindex].toFixed(1)+"KHz ",18,42,true); 101 | g.drawString("MIN : "+LOWBAND+"KHz ",18,54,true); 102 | g.drawString("MAX : "+HIGHBAND+"KHz ",18,66,true); 103 | g.setFontAlign(0,-1).drawString(MOD,160,12,true); 104 | 105 | } 106 | 107 | function drawSignal(){ 108 | g.setColor(Yellow); 109 | g.setFont('6x8').setFontAlign(-1,-1).drawString("RSSI: "+RSSI+" ",18,12,true); 110 | g.setFontAlign(-1,-1).drawString("SNR: "+SNR+" ",260,12,true); 111 | } 112 | 113 | function drawBat(){ 114 | var v = 7.15*analogRead(D35); 115 | g.setColor(Yellow).setFont('6x8').setFontAlign(-1,-1).drawString("BAT: "+v.toFixed(1)+"V",248,100,true); 116 | } 117 | 118 | function drawSSB(){ 119 | g.setColor(Grey).fillRect(0,0,319,239); 120 | g.clearRect(10,10,310,110); 121 | g.setColor(-1).setFont("6x8",2).setFontAlign(-1,0).drawString("KHz",270,50); 122 | VOLDISP.draw(); 123 | BANDSEL.draw(true); 124 | for (var i=0;i{TUNEORSTEP=!TUNEORSTEP; changeStep(0);}); 183 | ROTARY.handler = (inc) => { 184 | if (STATE==1){ 185 | if (TUNEORSTEP) { 186 | changeStep(inc); 187 | } else { 188 | TUNEDFREQ+=(inc*STEP); 189 | setTune(TUNEDFREQ); 190 | } 191 | } else if(STATE==0) { 192 | VOL+=inc; 193 | VOL=VOL<0?0:VOL>63?63:VOL; 194 | VOLDISP.update(VOL); 195 | RADIO.volume(VOL); 196 | } else if (STATE==2) { 197 | BANDSEL.move(inc); 198 | if (BANDS.length!=0) setBand(); 199 | } 200 | }; 201 | ROTARY.on("change",ROTARY.handler); 202 | RADIO.signalQPoll = setInterval(()=>{ 203 | var r = RADIO.getSQ(); 204 | SNR=r.snr; RSSI=r.rssi; STEREO=r.stereo; 205 | drawSignal(); 206 | drawBat(); 207 | },500); 208 | TC.touchHandler = (p) => { 209 | for (var j = 0; j < BUTTONS.length; ++j) 210 | if (BUTTONS[j].isTouched(p)) { 211 | BUTTONS[j].toggle(); 212 | return; 213 | } 214 | }; 215 | TC.on("touch",TC.touchHandler); 216 | } 217 | 218 | function clearControls(){ 219 | clearD33(); 220 | if (ROTARY.handler){ 221 | ROTARY.removeListener("change",ROTARY.handler); 222 | delete ROTARY.handler; 223 | } 224 | if (RADIO.signalQPoll) RADIO.signalQPoll = clearInterval(RADIO.signalQPoll); 225 | if (TC.touchHandler){ 226 | TC.removeListener("touch",TC.touchHandler); 227 | delete TC.touchHandler; 228 | } 229 | } 230 | 231 | eval(STOR.read("keyboard.js")); 232 | 233 | var KBD; 234 | 235 | function toKBD() { 236 | SCREEN=1; 237 | clearControls(); 238 | g.setColor(Grey).fillRect(0,0,319,239); 239 | KBD = new Keyboard(LOWBAND,HIGHBAND,toRADIO,true).init(); 240 | KBD.enable(true).draw(); 241 | } 242 | 243 | function toRADIO() { 244 | SCREEN=0; 245 | KBD.enable(false); 246 | if (KBD.valid()) { 247 | setTune(KBD.freq()*1000); 248 | } else { 249 | var f = KBD.freq(); 250 | var bi = BANDS.findIndex((e)=>{return f<=e.max && f>=e.min;}); 251 | if (bi>=0) { 252 | BANDSEL.pos=bi; 253 | BANDSEL.draw(true); 254 | setBand(f); 255 | } 256 | } 257 | delete KBD; 258 | setControls(); 259 | drawSSB(); 260 | } 261 | 262 | var SCREEN = 0; 263 | 264 | TC.swipeHandler = (dir) => { 265 | if (dir == TC.DOWN && SCREEN == 0) { 266 | toKBD(); 267 | return; 268 | } 269 | if (dir == TC.UP && SCREEN == 1) { 270 | toRADIO(); 271 | return; 272 | } 273 | }; 274 | 275 | TC.on("swipe",TC.swipeHandler); 276 | 277 | g.clear().setColor(-1).setFont("Vector",24).drawString("Loading SSB patch ...",40,100); 278 | if (initRADIO()){ 279 | drawSSB(); 280 | setControls(); 281 | } else g.clear().setColor(-1).setFont("Vector",24).drawString("FAILED to load SSB patch",10,100); 282 | 283 | -------------------------------------------------------------------------------- /ats25/ssbradioAGCnotworking.js: -------------------------------------------------------------------------------- 1 | eval(STOR.read("button.js")); 2 | eval(STOR.read("selector.js")); 3 | eval(STOR.read("bardisp.js")); 4 | 5 | var VOL=35; 6 | var STATE=0; //0 = VOLUME, 1 = FREQ, 2= BAND, 3 = BFO, 4 = ATT, 5 = DO Nothing 7 | var FREQ = 5450; 8 | var BFO = 0; 9 | var ATT = 0; 10 | var AGC = true; 11 | var RSSI =0; 12 | var SNR =0; 13 | var BANDNAME ="LW"; 14 | var LOWBAND = 4700; 15 | var HIGHBAND = 5500; 16 | var MOD="USB"; 17 | var SSB_MODE=0x9001; //AFC disable, AVC enable, bandpass&cutoff, 2.2Khz BW 18 | var STEP = 9; 19 | var BWindex = 1; 20 | var CAP =1; 21 | 22 | var buf = Graphics.createArrayBuffer(140,50,1,{msb:true}); 23 | 24 | var BANDS = (STOR.readJSON("bands.json")||[]).filter((e)=>{return e.mod!="AM";}); 25 | 26 | var BANDSEL = new Selector(BANDS,220,120); 27 | 28 | var VOLDISP = new BarDisp("VOL:",45,100,VOL); 29 | var ATTDISP = new BarDisp("ATT:",150,100,ATT); 30 | 31 | var BUTTONS=[ 32 | new Button("Step",80, 120, 60, 32, (b)=>{changeStep(b,0);}), 33 | new Button("BWid",80, 160, 60, 32, (b)=>{changeBW(b,1);}), 34 | new Button("Mute",80, 200, 60, 32, (b)=>{RADIO.mute(b);}), 35 | new Button("Tune",10, 120, 60, 32, (b)=>{setSelector(b,1,4,5,6,7);}), 36 | new Button("Vol",10, 160, 60, 32, (b)=>{setSelector(b,0,3,5,6,7);}), 37 | new Button("Band",10, 200, 60, 32, (b)=>{setSelector(b,2,3,4,6,7);}), 38 | new Button("BFO",150, 120, 60, 32, (b)=>{setSelector(b,3,3,4,5,7);}), 39 | new Button("ATT",150, 160, 60, 32, (b)=>{setSelector(b,4,3,4,5,6);}), 40 | new Button("AGC",150, 200, 60, 32, (b)=>{AGC=b;RADIO.setAGC(AGC,ATT);}), 41 | ]; 42 | 43 | BUTTONS[8].press = AGC; 44 | 45 | var stepindex= 2; 46 | const steps =[1,5,9,10]; 47 | function changeStep(b,n){ 48 | if (!b) return; 49 | stepindex = (stepindex+1)%4; 50 | STEP=steps[stepindex]; 51 | g.setColor(-1).setFont('6x8').setFontAlign(-1,-1).drawString("STEP: "+STEP+"KHz ",18,66,true); 52 | setTimeout(()=>{BUTTONS[n].reset();},200); 53 | } 54 | 55 | const bwidss =[1.2,2.2,3,4,0.5,1]; 56 | function changeBW(b,n){ 57 | if (!b) return; 58 | BWindex = (BWindex+1)%6; 59 | var pat = bwidss[BWindex]<2.5 ? BWindex : 0x10 | BWindex; 60 | SSB_MODE = (SSB_MODE & 0xFF00) | pat; 61 | RADIO.setProp(0x0101,SSB_MODE); 62 | g.setColor(-1).setFont('6x8').setFontAlign(-1,-1).drawString("Bwid: "+bwidss[BWindex].toFixed(1)+"KHz ",18,78,true); 63 | setTimeout(()=>{BUTTONS[n].reset();},200); 64 | } 65 | 66 | function drawFreq(){ 67 | buf.clear(); 68 | buf.setFont("Vector",42).setFontAlign(1,0).drawString((FREQ).toFixed(0),135,30); 69 | g.setColor(-1).drawImage(buf,120,20); 70 | } 71 | 72 | function setBFO(bv){ 73 | g.setColor(-1).setFont("6x8",2).setFontAlign(1,-1).drawString(" "+bv+" Hz",280,80,true); 74 | RADIO.setProp(0x0100,bv); 75 | BFO=bv; 76 | } 77 | 78 | function drawBand() { 79 | g.setColor(Yellow); 80 | g.setFont('6x8').setFontAlign(-1,-1).drawString("BAND: "+BANDNAME+" ",18,30,true); 81 | g.drawString("MIN : "+LOWBAND+"KHz ",18,42,true); 82 | g.drawString("MAX : "+HIGHBAND+"KHz ",18,54,true); 83 | g.drawString("STEP: "+STEP+"KHz ",18,66,true); 84 | g.drawString("Bwid: "+bwidss[BWindex].toFixed(1)+"KHz ",18,78,true); 85 | g.setFontAlign(0,-1).drawString(MOD,160,12,true); 86 | } 87 | 88 | function drawSignal(){ 89 | g.setColor(Yellow); 90 | g.setFont('6x8').setFontAlign(-1,-1).drawString("RSSI: "+RSSI+" ",18,12,true); 91 | g.setFontAlign(-1,-1).drawString("SNR: "+SNR+" ",260,12,true); 92 | } 93 | 94 | function drawBat(){ 95 | var v = 7.15*analogRead(D35); 96 | g.setColor(Yellow).setFont('6x8').setFontAlign(-1,-1).drawString("BAT: "+v.toFixed(1)+"V",248,100,true); 97 | } 98 | 99 | function drawSSB(){ 100 | g.setColor(Grey).fillRect(0,0,319,239); 101 | g.clearRect(10,10,310,110); 102 | g.setColor(-1).setFont("Vector",20).setFontAlign(-1,0).drawString("KHz",260,50); 103 | VOLDISP.draw(); 104 | ATTDISP.draw(); 105 | BANDSEL.draw(true); 106 | for (var i=0;i { 162 | if (STATE==1){ 163 | if (BFO!=0) setBFO(0); 164 | FREQ+=(inc*STEP); 165 | FREQ = FREQHIGHBAND?HIGHBAND:FREQ; 166 | setTune(FREQ); 167 | } else if(STATE==0) { 168 | VOL+=inc; 169 | VOL=VOL<0?0:VOL>63?63:VOL; 170 | VOLDISP.update(VOL); 171 | RADIO.volume(VOL); 172 | } else if(STATE==3) { 173 | BFO+=inc*10; 174 | BFO=BFO<-990?-990:BFO>990?990:BFO; 175 | setBFO(BFO); 176 | } else if(STATE==4) { 177 | ATT+=inc; 178 | ATT=ATT<0?0:ATT>63?63:ATT; 179 | ATTDISP.update(ATT); 180 | RADIO.setAGC(AGC,ATT); 181 | } else if (STATE==2) { 182 | BANDSEL.move(inc); 183 | if (BANDS.length!=0) setBand(); 184 | } 185 | }; 186 | ROTARY.on("change",ROTARY.handler); 187 | RADIO.signalQPoll = setInterval(()=>{ 188 | var r = RADIO.getSQ(); 189 | SNR=r.snr; RSSI=r.rssi; STEREO=r.stereo; 190 | drawSignal(); 191 | drawBat(); 192 | },500); 193 | TC.touchHandler = (p) => { 194 | for (var j = 0; j < BUTTONS.length; ++j) 195 | if (BUTTONS[j].isTouched(p)) { 196 | BUTTONS[j].toggle(); 197 | return; 198 | } 199 | }; 200 | TC.on("touch",TC.touchHandler); 201 | } 202 | 203 | function clearControls(){ 204 | clearD33(); 205 | if (ROTARY.handler){ 206 | ROTARY.removeListener("change",ROTARY.handler); 207 | delete ROTARY.handler; 208 | } 209 | if (RADIO.signalQPoll) RADIO.signalQPoll = clearInterval(RADIO.signalQPoll); 210 | if (TC.touchHandler){ 211 | TC.removeListener("touch",TC.touchHandler); 212 | delete TC.touchHandler; 213 | } 214 | } 215 | 216 | eval(STOR.read("keyboard.js")); 217 | 218 | var KBD; 219 | 220 | function toKBD() { 221 | SCREEN=1; 222 | clearControls(); 223 | g.setColor(Grey).fillRect(0,0,319,239); 224 | KBD = new Keyboard(LOWBAND,HIGHBAND,toRADIO,true).init(); 225 | KBD.enable(true).draw(); 226 | } 227 | 228 | function toRADIO() { 229 | SCREEN=0; 230 | KBD.enable(false); 231 | if (KBD.valid()) { 232 | setTune(KBD.freq()); 233 | SELECTED=-1; 234 | } 235 | delete KBD; 236 | setControls(); 237 | drawSSB(); 238 | } 239 | 240 | var SCREEN = 0; 241 | 242 | TC.swipeHandler = (dir) => { 243 | if (dir == TC.DOWN && SCREEN == 0) { 244 | toKBD(); 245 | return; 246 | } 247 | if (dir == TC.UP && SCREEN == 1) { 248 | toRADIO(); 249 | return; 250 | } 251 | }; 252 | 253 | TC.on("swipe",TC.swipeHandler); 254 | 255 | g.clear().setColor(-1).setFont("Vector",24).drawString("Loading SSB patch ...",40,100); 256 | if (initRADIO()){ 257 | drawSSB(); 258 | setControls(); 259 | } else g.clear().setColor(-1).setFont("Vector",24).drawString("FAILED to load SSB patch",10,100); 260 | 261 | -------------------------------------------------------------------------------- /ats25/testapp.js: -------------------------------------------------------------------------------- 1 | Graphics.prototype.drawRotRect = function(w, r1, r2, angle) { 2 | var w2=w/2, h=r2-r1, theta=angle*Math.PI/180; 3 | return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0], 4 | {x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta})); 5 | }; 6 | 7 | 8 | g.clear(); 9 | var count = 0; 10 | var oldcount=0; 11 | var cx = 160, cy = 120, h= 240; 12 | 13 | var buf = Graphics.createArrayBuffer(100,50,1,{msb:true}); 14 | function flip(){ 15 | g.setColor(-1).drawImage(buf,cx-50,cy-25); 16 | } 17 | 18 | 19 | function display(i){ 20 | //var start=Date.now(); 21 | count+=i; 22 | count = count<0?0:count>270?270:count; 23 | buf.clearRect(0,0,99,49); 24 | buf.setColor(-1); 25 | buf.setFont("Vector",48).setFontAlign(0,0).drawString(count,50,25); 26 | flip(); 27 | if(i>0) 28 | g.setColor(0,1,0).drawRotRect(3,110,121,count+225); 29 | else 30 | g.setColor(0.7,0.7,0.7).drawRotRect(3,110,121,count+226); 31 | //console.log("Time",Math.floor(Date.now()-start)); 32 | } 33 | 34 | /* 35 | function display(i){ 36 | var start=Date.now(); 37 | count+=i*10; 38 | count = count<0?0:count>270?270:count; 39 | buf.clearRect(0,0,99,49); 40 | buf.setColor(-1); 41 | buf.setFont("Vector",48).setFontAlign(0,0).drawString(count,50,25); 42 | flip(); 43 | if(i>0) 44 | for (var a=oldcount;acount;a--) g.setColor(0.7,0.7,0.7).drawRotRect(3,110,121,a+226); 47 | oldcount=count; 48 | console.log("Time",Math.floor(Date.now()-start)); 49 | } 50 | */ 51 | 52 | 53 | g.setColor(0.7,0.7,0.7).fillCircle(cx,cy,h/2); 54 | g.setColor(g.theme.bg).fillCircle(cx,cy,h/2-10).fillRect(0,h-36,319,h-1); 55 | display(0); 56 | 57 | 58 | ROTARY.on("change",display); 59 | 60 | 61 | g.setColor(1,1,0); 62 | g.fillCircle(20,20,5); 63 | g.fillCircle(300,20,5); 64 | g.fillCircle(20,220,5); 65 | g.fillCircle(300,220,5); 66 | //g.fillCircle(160,120,5); 67 | 68 | TC.on("touch", (p)=>{ 69 | console.log("touch x: "+p.x+" y:"+p.y); 70 | }); 71 | 72 | TC.on("swipe", (d)=>{ 73 | console.log("swipe d: "+d); 74 | if (d==TC.UP) display(+10); 75 | if (d==TC.DOWN) display(-10); 76 | }); 77 | 78 | TC.on("longtouch", (p)=>{ 79 | console.log("long touch"); 80 | }); 81 | 82 | function timeit(){ 83 | var start=Date.now(); 84 | g.clear(); 85 | return Math.floor(Date.now()-start); 86 | } 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /ats25/xpt2046.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function createXPT2046(){ 4 | 5 | var OBJ = {DOWN:1, UP:2, LEFT:3, RIGHT:4}; 6 | 7 | var calib = require("Storage").readJSON("xpt2046.json",1)||{xf:15,xoff:16,yf:11,yoff:35}; 8 | 9 | function readXY(){ 10 | if (D34.read()) return; 11 | setSPI1Freq(2000000) 12 | D5.reset(); 13 | SPI1.send([0x91,0]); 14 | var r = SPI1.send([0x91,0,0xD1,0,0]); 15 | SPI1.send([0x90,0]); 16 | D5.set(); 17 | setSPI1Freq(20000000); 18 | var xc = (r[3]*256+r[4])/8; 19 | xc = 240+calib.xoff-Math.floor(xc/calib.xf); 20 | xc = xc>239?239:xc<0?0:xc; 21 | var yc = (r[1]*256+r[2])/8; 22 | yc = Math.floor(yc/calib.yf) - calib.yoff; 23 | yc = xc>319?319:yc<0?0:yc; 24 | return {x:320-yc,y:xc}; 25 | } 26 | 27 | function watch(){ 28 | var first=readXY(); 29 | var last = first; 30 | var count = 0; 31 | var interval; 32 | var rewatch = function(){ 33 | if (interval) interval=clearInterval(interval); 34 | setWatch(watch,D34,{repeat:false,edge:"falling"}); 35 | } 36 | if (!first) {rewatch(); return;} 37 | interval = setInterval(()=>{ 38 | if(!D34.read()){ 39 | var t = readXY(); 40 | if(t) last = t; 41 | ++count; 42 | if (count>=20) { 43 | OBJ.emit("longtouch",last); 44 | rewatch(); 45 | } 46 | } else { 47 | if (count<20){ 48 | var xdiff = last.x-first.x; 49 | var axdiff = Math.abs(xdiff); 50 | var ydiff = last.y-first.y; 51 | var aydiff = Math.abs(ydiff); 52 | if (axdiff<10 && aydiff<10) { 53 | OBJ.emit("touch",last); 54 | } else if (axdiff>50 || aydiff>50){ 55 | OBJ.emit("swipe",(axdiff>=aydiff)?(xdiff>0?OBJ.RIGHT:OBJ.LEFT):(ydiff>0?OBJ.DOWN:OBJ.UP)); 56 | } 57 | } 58 | rewatch(); 59 | } 60 | },100); 61 | } 62 | 63 | setWatch(watch,D34,{repeat:false,edge:"falling"}); 64 | return OBJ; 65 | } 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /bands.json: -------------------------------------------------------------------------------- 1 | 2 | [ {"name":"LW","mod":"AM","min":153,"max":279,"freq":198,"step":9}, 3 | {"name":"MW","mod":"AM","min":522,"max":1701,"freq":1395,"step":9}, 4 | {"name":"BEACON","mod":"AM","min":280,"max":470,"freq": 284,"step":1}, 5 | {"name":"630M","mod":"LSB","min":472,"max": 479,"freq": 475,"step":1}, 6 | {"name":"160M","mod":"LSB","min":1800,"max":1910,"freq":1899,"step":1}, 7 | {"name":"120M","mod":"AM","min":2300,"max":2500,"freq":2400,"step":5}, 8 | {"name":"90M","mod":"AM","min":3200,"max":3400,"freq":3300,"step":5}, 9 | {"name":"80M","mod":"LSB","min":3500,"max":3800,"freq":3630,"step":1}, 10 | {"name":"75M","mod":"AM","min":3900,"max":4050,"freq":3950,"step":5}, 11 | {"name":"60M","mod":"USB","min":4700,"max":5500,"freq":5450,"step":1}, 12 | {"name":"49M","mod":"AM","min":5800,"max":6300,"freq":6000,"step":5}, 13 | {"name":"40M","mod":"LSB","min":7000,"max":7200,"freq":7185,"step":1}, 14 | {"name":"41M","mod":"AM","min":7200,"max":7600,"freq":7210,"step":5}, 15 | {"name":"31M","mod":"AM","min":9300,"max":9995,"freq":9600,"step":5}, 16 | {"name":"30M","mod":"USB","min":10100,"max":10150,"freq":10125,"step":1}, 17 | {"name":"25M","mod":"AM","min":11500,"max":12200,"freq":11700,"step":5}, 18 | {"name":"22M","mod":"AM","min":13500,"max":13900,"freq":13700,"step":5}, 19 | {"name":"20M","mod":"USB","min":14000,"max":14350,"freq":14250,"step":1}, 20 | {"name":"19M","mod":"AM","min":15000,"max":15900,"freq":15700,"step":5}, 21 | {"name":"17M","mod":"USB","min":18068,"max":18168,"freq":18100,"step":1}, 22 | {"name":"16M","mod":"AM","min":17400,"max":17995,"freq":17600,"step":5}, 23 | {"name":"15M","mod":"AM","min":18800,"max":19100,"freq":18950,"step":5}, 24 | {"name":"15M","mod":"USB","min":21000,"max":21450,"freq":21350,"step":1}, 25 | {"name":"13M","mod":"AM","min":21450,"max":21995,"freq":21500,"step":5}, 26 | {"name":"12M","mod":"USB","min":24890,"max":24990,"freq":24940,"step":1}, 27 | {"name":"11M","mod":"AM","min":25500,"max":26200,"freq":25800,"step":5}, 28 | {"name":"CB","mod":"AM","min":26200,"max":27990,"freq":27200,"step":1}, 29 | {"name":"10M","mod":"USB","min":28000,"max":30000,"freq":28500,"step":1}, 30 | {"name":"SW","mod":"AM","min":1730,"max":30000,"freq":15500,"step":5}, 31 | {"name":"SSB","mod":"USB","min":1730,"max":30000,"freq":18100,"step":1}, 32 | {"name":"LOW","mod":"SYN","min":153,"max":1701,"freq":198,"step":1} 33 | ] 34 | 35 | 36 | -------------------------------------------------------------------------------- /boot0.js: -------------------------------------------------------------------------------- 1 | eval(require("Storage").read("main.js")); 2 | 3 | -------------------------------------------------------------------------------- /bootcde.js: -------------------------------------------------------------------------------- 1 | eval(STOR.read("chooser.js")); 2 | 3 | -------------------------------------------------------------------------------- /encoder.js: -------------------------------------------------------------------------------- 1 | function createEncoder(pinA,pinB){ 2 | pinMode(pinA,"input_pullup"); 3 | pinMode(pinB,"input_pullup"); 4 | var a0=pinA.read(), c0=pinB.read(), incr0 =0, second=false; 5 | var OBJ = {}; 6 | 7 | function handler () { 8 | var a = pinA.read(); 9 | var b = pinB.read(); 10 | if (a != a0) { // A changed 11 | a0 = a; 12 | if (b != c0) { 13 | c0 = b; 14 | var incr = (a == b)?-1:1; 15 | if (incr!=incr0 || !second) OBJ.emit("change",incr); 16 | incr0=incr; second = !second; 17 | } 18 | } 19 | } 20 | setWatch(handler,pinA,{repeat:true,edge:"both"}); 21 | return OBJ; 22 | } 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /espruino_2v13.11_esp32/README_flash.txt: -------------------------------------------------------------------------------- 1 | How to flash Espruino ESP32 2 | =========================== 3 | 4 | *** To flash an ESP32 using the serial port use: 5 | python ${ESP_IDF_PATH}/components/esptool_py/esptool/esptool.py \ 6 | --chip esp32 \ 7 | --port "/dev/ttyUSB0" \ 8 | --baud 921600 \ 9 | write_flash \ 10 | -z \ 11 | --flash_mode "dio" \ 12 | --flash_freq "40m" \ 13 | 0x1000 bootloader.bin \ 14 | 0x10000 espruino_esp32.bin \ 15 | 0x8000 partitions_espruino.bin 16 | 17 | On windows, its an option to use flash tools from espressif found here: 18 | http://espressif.com/sites/default/files/tools/flash_download_tools_v3.4.4.zip 19 | -------------------------------------------------------------------------------- /espruino_2v13.11_esp32/bootloader.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/espruino_2v13.11_esp32/bootloader.bin -------------------------------------------------------------------------------- /espruino_2v13.11_esp32/espruino_esp32.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/espruino_2v13.11_esp32/espruino_esp32.bin -------------------------------------------------------------------------------- /espruino_2v13.11_esp32/partitions_espruino.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/espruino_2v13.11_esp32/partitions_espruino.bin -------------------------------------------------------------------------------- /image/amapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/amapp.jpg -------------------------------------------------------------------------------- /image/amradio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/amradio.jpg -------------------------------------------------------------------------------- /image/ats100.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/ats100.jpg -------------------------------------------------------------------------------- /image/ats100home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/ats100home.jpg -------------------------------------------------------------------------------- /image/ats25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/ats25.jpg -------------------------------------------------------------------------------- /image/dclick.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/dclick.jpg -------------------------------------------------------------------------------- /image/fmapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/fmapp.jpg -------------------------------------------------------------------------------- /image/fmradio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/fmradio.jpg -------------------------------------------------------------------------------- /image/home_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/home_screen.jpg -------------------------------------------------------------------------------- /image/keys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/keys.jpg -------------------------------------------------------------------------------- /image/ssbapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/ssbapp.jpg -------------------------------------------------------------------------------- /image/ssbradio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffmer/SI4732_Espruino/9a7814b0301be22e5ce6b0062329b976c4451f1d/image/ssbradio.jpg --------------------------------------------------------------------------------