Tuesday, February 27, 2024

Youtube Alternative

Recently, I was listening to a longish classical music piece on Youtube. Right in the middle of a movement, the video stops to show an ad. How annoying! I don't use Youtube very often, so I don't see the sense in buying Youtube Premium. However, for the few things that I do watch, I don't want the videos interrupted by ads in the middle. First, that should just be against the law, second, I wrote my own front end to Youtube -- no ads!


The parts are pretty simple, it's just search, display results, then show the selected video. I wrote it all in Java as that's what I'm most familiar with. The actual playing of videos is passed off to a local video player, both mpv and vlc work well for playing an http stream.


The complete code is here, what follows is the description of what it does.


Search on Youtube is just this URL:


https://www.youtube.com/results?search_query=search+words+here


The search results are mostly a lot of json, so a few regular expressions work well to extract the relevant content. In the code, the first regex pulls in those sections of the json that cover the videos, the subsequent regular expressions separate out the individual parts of each section. So a 'section' has the video ID, a url for a thumbnail, and a description of the video from the accessibility content. I display these with a button to play the video and a second button to download the video.


Getting a video from Youtube is just this URL;


https://www.youtube.com/watch?v=videoID


Both mpv and vlc will play an http stream from a url just by passing the url on the command line, like


mpv https://www.youtube.com/watch?v=Z8zk7XKyoE8


For the download, I'm using yt-dlp. I added a quick and dirty menu item to set the download directory. It's not a file chooser, you have to type in the path yourself, so yeah, not good. The command line for yt-dlp looks like this:

yt-dlp -P downloadDirectory https://www.youtube.com/watch?v=videoId


Here's a screenshot:




That's it. Quick and dirty, but useful. 

Tuesday, July 11, 2023

Install Kodi on Raspberry Pi from Linux

A short article about how to install Kodi on a Raspberry Pi from Linux. I used to use the LibreELEC USB-SD Creator program, but it no longer works on Linux or Mac. The LibreELEC forum says that's due to too many dependencies on Qt. Oh well, it's not that hard to install without the spiffy GUI. There are probably other ways to get LibreELEC on a Raspberry Pi, but this is what worked for me and is pretty straightforward


The Raspberry Pi I have is somewhat older now, maybe 4 years. For some reason, it gave up the ghost and wouldn't boot. I'd just get the LibreELEC start up screen, then after a while, kernel panic and nothing more. So I went to reinstall the OS, that's when I found out the GUI installer no longer works, even the one I had previously installed on my laptop. 


2 GB Raspberry Pi

I put it in this really nice box, just for Kodi:



I'm running Arch Linux, but these instructions should work on any Linux distro, and would work for a USB stick also:


  1. Download the latest image of LibreELEC from https://libreelec.tv/downloads/raspberry/

    Current image is LibreELEC-RPi4.arm-11.0.1.img.gz, but get whatever is the latest.

  2. Unzip the image: tar -xvzf LibreELEC-RPi4.arm-11.0.1.img.gz

  3. Insert an empty SD card into the computer. Find it with lsblk, it's probably /dev/sdb, but check to be sure for the next step.

  4. Burn the image to the SD card with (change the sdX as needed):

    sudo dd if=LibreELEC-RPi4.arm-11.0.1.img  of=/dev/sdX bs=4M

    Of course, use the actual filename of the image you downloaded and unzipped.

  5. Plug the SD card into the Kodi box and boot. That should be it.

Tuesday, November 8, 2022

How to add a URL to the home screen on Android

Yeah, so this took me an hour or so to figure out. I live on a beach on the ocean, and I have a boat, so I want to know the tides. The most accurate website I've found for my area is tideking.com. On my laptop, I have a bookmark for the right page on their site, which, of course, goes right to it every time. On my phone, I tried the usual -- go to the site, find the right page, click the hamburger (the 3 dot thing), and click add to home screen. Yep, there is a link on my home screen, but it goes to their main page, not my specific page. I tried this in Chrome, Firefox, and Brave, all the same, it takes me to someplace I've never heard of, not my bookmarked place. Apparently, this is controlled by the website, not me, which is bullshit.

Here is the best answer to go to any URL directly from your home screen:

https://play.google.com/store/apps/details?id=com.deltacdev.websiteshortcut

It's an app called Website Shortcut. It's free, it doesn't have any ads, and it works as expected. Go to your page, copy the URL, open Website Shortcut, paste it in, pick an icon, and give it a name. Put it wherever you want on your home screen Done! It goes directly to your desired page, no problems. I rated it 5 stars.


Thursday, September 29, 2022

A short review of the new(ish) HP Dev One laptop

 I finally put my ageing ThinkPad T420 to rest. It was a great laptop and served me well for 11 (eleven!!!) years. It did almost everything I wanted, but it was getting old and slow, much like me. It seemed it was taking longer and longer to compile code, and it couldn't process 4k video from my GoPro camera. I'd read some great reviews about the HP Dev One laptop, so I ordered one, and I can't say I'm disappointed at all. 


What I like:

Just about everything. It came with Pop!_OS, which I tried for about an hour then installed Arch, because that's what I've been running for years. Nothing against Pop, it's just not my thing.

It's fast, the screen is very readable, the keyboard is okay. I grew up on IBM Model M keyboards, so I usually use an external clicky keyboard. I'm currently using a WASD keyboard with Cherry MX Green, which feels a lot like my old Model M's.

The touchpad is great, although I do tend to use an external mouse when I can.


What I don't like:

Well, nothing really. The only nit I have is that the function keys are not function keys by default. Instead, the "action" keys, that is, the volume, brightness, etc, keys are the default. This is a laptop aimed at developers, and I use the function keys all the time. I have all of them, F1 through F12 mapped to some action in my code editor. Fortunately, there is a BIOS setting to change it so the function keys are function keys by default. I would have thought someone at HP would have thought of having it the other way around, since I'm sure I'm not alone in using the crap out of function keys.

Sunday, September 4, 2022

Javadoc

 Yay! Another post in the same year!


I've been writing java code since the beginning of Java. The API documentation has been fantastic, it's all in a set of web pages that can be downloaded locally so I can browse it any time I want. Since Java 10 or so, though, the web pages suck. There used to be frames with a tree on the left, which made it easy to click around and find things. The new pages have a search box and no tree, so you have to know what you're looking for, no more discovery. 


Well, today I got tired of using Java 8 javadoc since it's way out of date. I downloaded the Java 18 javadoc. It still sucks for navigation. A google search led me to https://github.com/climber09/Javadoc-Frames-Generator. This works like a charm! It depends on Phantomjs, which is no longer under development, but that doesn't matter because it works well in its current state. So the steps to install:


  1. download and unzip phantomjs somewhere.
  2. download and unzip javadoc-frames-generator somewhere.
  3. in a terminal, cd to the javadoc-frames-generator directory
  4. run /path/to/phantomjs ./jf-generator.js /path/to/javadocs/docs/api
  5. wait for it to finish
  6. open /path/to/javadocs/docs/api/main.html in your browser. Bookmark it for later use. Done!

A big thanks to James P. Hunter for creating this.

Friday, August 19, 2022

Printer set up

Wow, it's been over a year since I've posted, I guess I've been busy with other things.

So I recently got a new laptop. My old T420 Thinkpad was definitely showing it's age. Even with an SSD, it was just slow. My new one is an HP Dev One, and it is very nice and speedy. It came with Pop!_OS, which I tried for about an hour then installed Arch, because that's what I've been running for years.

Of course, no printers were installed, and it always seems like it takes me a while to figure out the right URL for the network printers. Then I was reading through the CUPS documentation, and found this gem:

/usr/lib/cups/backend/snmp 192.168.4.46

Change the IP address as needed, but you'll get back a lot of info about the printer, in particular, the specific URL you need to enter into CUPS:

INFO: Using default SNMP Community public

network lpd://192.168.4.46:515/PASSTHRU "EPSON XP-630 Series" "EPSON XP-630 Series" "MFG:EPSON;CMD:ESCPL2,BDC,D4,D4PX,ESCPR2,END4;MDL:XP-630 Series;CLS:PRINTER;DES:EPSON XP-630 Series;CID:EpsonRGB;FID:FXN,DPA,WFA,ETN,AFN,DAN,WRA;RID:20;DDS:022500;ELG:0D02;SN:573541593138373069;" ""

And there is the answer, lpd://192.168.4.46:515/PASSTHRU. I pasted that into CUPS "add a printer" setup, and it worked perfectly! Plus it shows the model number of the printer, which is also handy.

Friday, July 23, 2021

Arch Linux How to fix "Unrecognized archive format"

 I went to update my Arch Linux installation this morning, and got this error:


danson@deadlock:$ sudo pacman -Syu

:: Synchronizing package databases...

 core                614.0 KiB  1438 KiB/s 00:00 [-------------------------------] 100%

 extra               614.0 KiB  1498 KiB/s 00:00 [-------------------------------] 100%

 community           614.0 KiB  1204 KiB/s 00:01 [-------------------------------] 100%

:: Starting full system upgrade...

error: could not open file /var/lib/pacman/sync/core.db: Unrecognized archive format

error: could not open file /var/lib/pacman/sync/extra.db: Unrecognized archive format

error: could not open file /var/lib/pacman/sync/community.db: Unrecognized archive format

 there is nothing to do


After a bit of research, it turns out there is some problem with the mirror list. The list is stored in /etc/pacman.d. The fix is to "simply" generate a new mirror list. A little more research led me to


which is a web page that will generate a new mirror list for you. Uncomment the lines you like, then save it to /etc/pacman.d/mirrorlist, overwriting your old mirrorlist file. 

Next, delete the .db files from /var/lib/pacman/sync, then run

pacman -Syyu

and new .db files will be generated and the system update should work fine.

Monday, June 7, 2021

Moving the tabs in the new Firefox

 So I upgraded my laptop today, the usual pacman -Syu, and got the new version of Firefox, version 89.0. For some reason, browser makers want to put the tabs ABOVE the address bar, which makes no sense, and lots of people are whining about it on the internet.  I did a bunch of googling and found this solution from Mozilla. That almost worked for this new version, but not quite, and they left out a major step. 

Here's the procedure: 

  1. In Firefox in the address bar, type about:support. Make a note of the location of your profile directory.
  2. Go to profile directory from step 1. Create a subdirectory named "chrome" if it doesn't already exist.
  3. Copy the userChrome.css mention in the solution link above by user cor-el (thanks, cor-el! I've also pasted my version below in case cor-el's version disappears for some reason).
  4. Paste the css you copied in step 3 into a file named 'userChrome.css" into the "chrome" directory you created in step 2.
  5. Open the userChrome.css file in your favorite editor (I recommend jEdit, of course).
  6. On line 20, there should be an entry for "--menubar-height", it is 0, which moved the tabs in the way of my bookmark bar. If you don't have the bookmark bar, you're probably good to go, almost -- keep reading. If you do, I changed --menubar-height to 41px. You'll also need to make this change in line 30 if you use full screen mode.
  7. I noticed that if I open several tabs, the tab bar would not expand across the window as expected. I changed line 45, width: 100wv to width: 1000wv, and now the tab bar expands across the window as expected.
  8. Save the file.
  9. In the Firefox address bar, type about:config, search for "toolkit.legacy", there should only be one result, toolkit.legacyUserProfileCustomizations.stylesheets. Change it to "true". This is necessary so Firefox will actually read your userChrome.css file at start up.
  10. Restart Firefox, you should be good to go.

I also noticed after doing this that the tab height is a little too short, so on line 14, I changed the --tab-min-height setting to 30 instead of 25. It also seems like the text in the tab isn't centered well vertically, so on line 54, I changed the 1px to 5 px, that looks better, I think.

You can play around with the other settings as needed to make you happy. I wasn't able to figure out how to make the tabs narrower by default, but they do resize correctly when you have many tabs open.

Here is the full contents of my userChrome.css file:



@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");

/* TABS: bottom - Firefox 65 and later - updated for 89+ */
/* https://searchfox.org/mozilla-release/source/browser/themes/shared/tabs.inc.css */
/* https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox */

/* ROOT - VARS */
/* you can adjust the CSS variables until it looks correct */
/* you can use the Browser Toolbox to get the toolbar heights */

*|*:root {
  --tab-toolbar-navbar-overlap: 0px !important;

  --tab-min-height: 30px !important;
  --tab-min-width:  60px !important;

  --tab-caption: -5px; /* caption buttons on tab bar */
  --tab-adjust:   3px; /* adjust tab bar */

  --menubar-height: 41px; /*30px|41px=11px; caption buttons on menubar*/
  --navbar-height:  31px; /*31px*/
  --bookmarkbar-height: 26px;

  --tabbar-top: calc(var(--menubar-height) + var(--navbar-height) + var(--bookmarkbar-height) + var(--tab-adjust)); /*89+*/
}

/* in case you enable extra toolbars in full screen mode */
*|*:root[inFullscreen] {
  --tab-adjust: 3px;
  --menubar-height: 0px; /*30px*/
  --navbar-height:  31px; /*31px*/
  --bookmarkbar-height: 0px; /*26px*/
  --tabbar-top: calc(var(--menubar-height) + var(--navbar-height) + var(--bookmarkbar-height) + var(--tab-adjust)); /*89+*/
}

/* TAB BAR - below nav-bar */
#navigator-toolbox toolbar:not(#nav-bar):not(#toolbar-menubar) {-moz-box-ordinal-group:10}
#TabsToolbar {-moz-box-ordinal-group:1000}

#TabsToolbar {
  display: block !important;
  position: absolute !important;
/*  bottom: 0 !important; /* 68-88: BOTTOM */
  top: var(--tabbar-top);  /* 89+: TOP */
  width: 1000vw !important;
}

#tabbrowser-tabs {
  width: 100vw !important;
}

/* navigator-toolbox - PADDING */
*|*:root:not([chromehidden*="toolbar"]) #navigator-toolbox {
  padding-bottom: calc(var(--tab-min-height) + 1px) !important; /*ADJUST*/
  background-color: var(--toolbar-bgcolor) !important;
}

/* TabsToolbar with menubar and titlebar hidden - rules for Firefox 65-73 */
*|*:root[tabsintitlebar]:not([inFullscreen="true"]):not([sizemode="maximized"])
 #toolbar-menubar[autohide="true"] ~ #TabsToolbar{
}

/* TABS: height */
#tabbrowser-tabs,
#tabbrowser-tabs > .tabbrowser-arrowscrollbox,
.tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] {
  min-height: var(--tab-min-height) !important;
  max-height: var(--tab-min-height) !important;
}

#TabsToolbar {
  height: var(--tab-min-height) !important;
  margin-bottom: 1px !important;
  box-shadow: ThreeDShadow 0 -1px inset, -moz-dialog 0 1px !important; /*OPTIONAL*/
  background-color: var(--toolbar-bgcolor) !important;
  color:            var(--toolbar-color) !important;
  z-index: 1 !important;
}

/* indicators *//*
*|*:root[privatebrowsingmode=temporary] .private-browsing-indicator {
  position: absolute !important;
  display: block !important;
  right: 0px !important;
  bottom: 0px !important;
  width: 14px !important;
  pointer-events: none !important;
}
*/
.private-browsing-indicator {display: none !important;}
.accessibility-indicator    {display: none !important;}

/* Indicators - HIDE *//*
*|*:root:not([accessibilitymode])             .accessibility-indicator    {display: none !important}
*|*:root:not([privatebrowsingmode=temporary]) .private-browsing-indicator {display: none !important}
*/

/* Drag Space */
.titlebar-spacer[type="pre-tabs"],
.titlebar-spacer[type="post-tabs"] {
  width: 20px !important;
}

/* Override vertical shifts when moving a tab */
#navigator-toolbox[movingtab] > #titlebar > #TabsToolbar {
  padding-bottom: unset !important;
}

#navigator-toolbox[movingtab] #tabbrowser-tabs {
  padding-bottom: unset !important;
  margin-bottom: unset !important;
}

#navigator-toolbox[movingtab] > #nav-bar {
  margin-top: unset !important;
}

/* Hide window-controls and caption buttons on Tab Bar */
#TabsToolbar #window-controls {display: none !important;}
#TabsToolbar .titlebar-buttonbox-container {display: none !important;}

Wednesday, May 26, 2021

How to install and remove KDE on Arch Linux

 So I was just playing around, I used to run KDE all the time (years ago) and thought I'd give it another try. 


Installation was easy:

sudo pacman -S xorg plasma plasma-wayland-session kde-applications


It turns out that KDE still isn't for me, it's a little slow and clunky for my 10 year old laptop, so I'll remove it. That's a little harder:


sudo pacman -Rns plasma plasma-wayland-session kde-applications


Didn't work. Notice I didn't include xorg, I probably didn't need it installed along with KDE since I already had it all installed. The problem was dependencies.


So I tried just uninstalling plasma:


sudo pacman -Rns plasma plasma-wayland-session


Nope:

error: failed to prepare transaction (could not satisfy dependencies)
:: removing libksysguard breaks dependency 'libksysguard' required by kdevelop
:: removing plasma-workspace breaks dependency 'plasma-workspace' required by kget
:: removing libkscreen breaks dependency 'libkscreen' required by lxqt-config
:: removing plasma-workspace breaks dependency 'plasma-workspace' required by telepathy-kde-desktop-applets

 

So I'll just remove kdevelop:


danson@deadlock:~$ sudo pacman -Rns kdevelop
checking dependencies...
error: failed to prepare transaction (could not satisfy dependencies)
:: removing kdevelop breaks dependency 'kdevelop' required by kdevelop-php
 

Okay, I'll just remove kdevelop-php:

danson@deadlock:~$ sudo pacman -Rns kdevelop-php
checking dependencies...
error: failed to prepare transaction (could not satisfy dependencies)
:: removing kdevelop-php breaks dependency 'kdevelop-php' required by umbrello

So remove umbrello: Yep! And it removed kdevelop and kdevelop-php at the same time!
danson@deadlock:~$ sudo pacman -Rns umbrello


Try to remove plasma again:

danson@deadlock:~$ sudo pacman -Rns plasma plasma-wayland-session
checking dependencies...
error: failed to prepare transaction (could not satisfy dependencies)
:: removing plasma-workspace breaks dependency 'plasma-workspace' required by kget
:: removing libkscreen breaks dependency 'libkscreen' required by lxqt-config
:: removing plasma-workspace breaks dependency 'plasma-workspace' required by telepathy-kde-desktop-applets

Okay, so whack kget: Yep!
danson@deadlock:~$ sudo pacman -Rns kget


Try plasma again:

danson@deadlock:~$ sudo pacman -Rns plasma plasma-wayland-session
checking dependencies...
error: failed to prepare transaction (could not satisfy dependencies)
:: removing libkscreen breaks dependency 'libkscreen' required by lxqt-config
:: removing plasma-workspace breaks dependency 'plasma-workspace' required by telepathy-kde-desktop-applets
danson@deadlock:~$ sudo pacman -Rns telepathy-kde-desktop-applets

That worked, also removed lxqt-config, then reinstalled it, since I'm going to be running lqxt.

Now plasma goes away!

danson@deadlock:~$ sudo pacman -Rns plasma plasma-wayland-session


Reinstall lxqt-config:

danson@deadlock:~$ sudo pacman -S lxqt-config

And finally remove the KDE application packages:

danson@deadlock:~$ sudo pacman -Rns kde-applications


That's it. Not particularly straightforward, but easy enough to get all this stuff off of my laptop.



Tuesday, September 29, 2020

Freedb, the CD database is gone, here are alternatives

Freedb shutdown earlier this year, and it was the default CDDB for Asunder, which is what I usually use for ripping CDs. 

I found two alternatives:

  1. gnudb.gnudb.org, port 8880
    This is a replacement for freedb, with the stated goal of keeping the database alive, free, and active. 
  2. freedb.freac.org, port 80
    This is another replacement for freedb, and seems to be somewhat more complete than gnudb.

In Asunder, go to Preferences - Advanced, and fill in one of these server names and port.

Saturday, June 13, 2020

Arduino IDE and ctags

This took me a while to figure out -- suddenly, sketches that have been compiling and uploading stopped compiling. The error message was useless:

exit status 1
Error compiling for board WeMos D1 R1.


I've been using jEdit as my editor for Arduino, there is a setting in the Arduino IDE to use an external editor, and that's what I've been doing. Since *.ino files are essentially C or C++ files, I wanted to use the ctags sidekick in jEdit to see the various declarations. As *.ino is not a standard file name extension for ctags, I added it in my ~/.ctags file like this:

--langmap=C++:+.ino

It turns out that didn't work for jEdit, for some reason, the plugin doesn't pick up that setting. In the plugin options for the ctags sidekick, there is a place to enter ctags invocation options, so I entered the above line and now the sidekick properly parses *.ino files. Great!

Not so great. Now nothing will compile in Arduino IDE. It turns out the IDE also uses ctags, and that line in my ~/.ctags file caused the problem. Since I put the same line in jEdit, I didn't really need the .ctags file any more, so I deleted it and sketches compiled and uploaded again. Weird!

Friday, June 12, 2020

How to Compile iSpindel with Arduino IDE


My next little electronics project is to build an iSpindel. This is an open source project for DIY people to build their own Tilt-like specific gravity monitor for beer brewing, so this combines two of my favorite hobbies, brewing and electronics. The basic idea is as the beer ferments the angle of the iSpindel device sinks to a lower angle, and then does some math to figure out the specific gravity. The idea isn't so much to have exact gravity readings, but to know when fermentation is done by seeing the gravity flat-line.

iSpindel is set up to compile on PlatformIO, but I don't want to have to install and learn how to use a new IDE, so I'm going to do it in Arduino IDE. I realize the project has firmware files that are already compiled and ready to upload, but I want to make a couple of small changes, so I want to be able to compile it myself. The license says it's okay for me to do that:

"This project is free to use. It's permitted to modify for personal use.
It's not permitted to distribute the modified project. Modification can be
distributed via the official iSpindel release only. It's not permitted to distribute
 in a commercial way without permission."

So I'll make my little changes, and I won't share them with anyone. Hmm.

Start at https://github.com/universam1/iSpindel
Download the project as a zip file.
Unzipped to ~/src/iSpindel-master

Go to ~/src/iSpindel-master/pio/src, rename iSpindel.cpp to iSpindel.ino

Open iSpindel.ino in Arduino IDE, it will asks to make a folder named iSpindel and move the ino file into it. Do it.

In the ~/src/iSpindel-master/pio/lib folder, package these as individual zip files:
DoubleResetDetector
Globals
MPUOffset
Sender
WiFiManagerKT
tinyexpr

Install these into Arduino IDE by going to Sketch, Include Library, Add .zip Library, select the zip files. You have to do this one file at a time, and the IDE doesn't remember where it was, so it's kind of a pain.

Attempting to compile at this point gives an error about missing PubSubClient.h, downloaded from
https://github.com/knolleary/pubsubclient/releases/tag/v2.8
Download the zip file, not the tar.gz
In Arduino IDE, go to Sketch, Include Library, Add .zip library, select the downloaded zip file.

Skip this step, see below.
Attempting to compile at this poing give an error about missing I2Cdev.h.
Go to https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/I2Cdev
Download I2Cdev.cpp and I2Cdev.h, make a zip file with these, then do the Include Library thing again for this zip file.
I downloaded them to ~/src/iSpindel-master/pio/lib/I2Cdev.

Now missing MPU6050.h:
Go to Sketch, Include Library, Library Manager, search for MPU6050, install the one from Electronic Cats, version 0.0.2.

Now a fatal error, OneWire.h not found, installed DS18B20_RT library version 0.1.6, from Library Manager, it installs OneWire version 2.3.5 as a dependency. This is for the temperature sensor.

Also, multiple libraries found for I2Cdev.h:
Multiple libraries were found for "I2Cdev.h"
 Used: ~/Arduino/libraries/I2Cdev
 Not used: ~/Arduino/libraries/MPU6050

I'm going to delete the Used one and use the Not used one. I just deleted the folder ~/Arduino/libraries/I2Cdev, recompiled, and that error went away. Just skip installing I2Cdev as I mentioned above and this won't be a problem.

Next, DallasTemperature.h not found. Go to Sketch, Include Library, Library Manager, search for Dallas Temperature, install version 3.8.0.

Next, RunningMedian.h not found. Go to Sketch, Include Library, Library Manager, search for RunningMedian, install version 0.2.0.

Next, ThingSpeak.h not found. Install version 1.5.0 from Library Manager.

Next, BlynkSimpleEsp8266.h. Install BlynkGSM_Manager version 1.0.9 from Library Manager. It wants to also install Blynk version 0.6.1 and TinyGSM version 0.10.5, install all.

Yay! It compiled! There is one warning:

WARNING: library MPU6050 claims to run on avr, samd architecture(s) and may be incompatible with your current board which runs on esp8266 architecture(s).


Update, June 14, 2020: It compiles. It uploads. It doesn't work. Well, it does, but it's impossible to get to the configuration. I spent the good part of a day trying to get it to work, but no luck. It hangs on fetching the stored json configuration from the file system, but of course, there is no stored json yet since I can't get to the configuration. Hacked around that, then the DoubleResetDetector doesn't work, it just doesn't detect. I'm going to start from scratch and write my own code, borrowing from the iSpindel code as needed. Reading through that code, I can see there is a lot of checking for this and that that really isn't necessary. I pretty much already know how to set up the wifi, read the temperature, and read the motion/gyro/accelerometer. What I don't have is the experience the iSpindel folks have to figure out the math to calculate specific gravity, so I'll be "borrowing" that code.



Wednesday, June 3, 2020

Wifi Volume Control

Mostly I play music on my home stereo system via Kodi and the Android Yatse app. Yatse lets me adjust the volume, but if I'm playing something on Youtube or Pandora or even Facebook, there is no way for me to remotely adjust the volume for my outside speakers. So I built a little volume controller and added it to my speaker controller. This turned out to be a fun little project.

First, a couple of pictures:

This shot shows the inside of my speaker controller. There are a lot of parts, I used an old computer case to hold everything together. My original speaker controller is in the middle with all the wires and the blue relays. The box on the right is another speaker switcher, which I kept as part of the system since it does impedance balancing that my controller doesn't do. At the very top are three individual volume knobs, one for each speaker zone around the house. These are manual knobs, there isn't a way to control them remotely. See the wires coming out of the Arduino and going around to the right under the black box? Those are the connections for the volume controller -- power, ground, data, and clock.

This picture shows the volume controller that I built. It's the little green circuit board on the right. It works as a "master" volume in that it adjusts the volume for all three zones at once. This turns out to be fine, it's really the same thing that Yatse does for Kodi, but now I can adjust the volume for other apps as well.


























Here's the layout for the volume controller:

What's missing here???
























I followed the schematic very closely, but I forgot that the speaker connections have TWO wires each, they all have a ground in addition to in/out. Also, the above has the ground from the PT2257 going to the wrong place. So this is a better picture:

This one has connections for the ground wires.
























I added the code to run this to the code for the speaker controller, so it's all in one place. Here are the parts that I added:

In the "html head" section, I added a little bit of javascript:

    server.println("<script>");
    server.println("function doit(){");
    server.println("document.getElementById(\"vF\").submit();}");
    server.println("</script>


Then in the main body of the html I added a slider to set the volume:

    server.print("</div><div class='container'><h3>Master Volume</h3><form id='vF' action='/' role='form'>");
    server.print("<input type='range' name='volume' min='0' max='79' value='");
    server.print(volume);
    server.print("' oninput='doit(value)'>");
    server.print("</form>");
    server.print("<form id='mF' action='/' role='form'>");
    server.print("<input type='submit' name='mute' value='Mute'/></form>


To handle the form data, I added these lines to the "handleRequest" method:

    if (request.indexOf("volume=") > -1) {
        int start = request.indexOf("volume=") + 7; // 7 is length of "volume="
        int end = request.indexOf(" ", start);
        volume = request.substring(start, end).toInt();
    } else {
        if (request.indexOf("mute") > -1) {
            volume = 0;
        }
    }



There are several libraries around for running the PT2257, but it's pretty simple and doesn't really need a whole library for what I want to do, so I only needed to add these few methods:

void setVolume(int level) {
    byte bbbaaaa = calculateVolume(level);
    byte aaaa = bbbaaaa & 0b00001111;
    byte bbb = (bbbaaaa >> 4) & 0b00001111;

    Wire.beginTransmission(EVC_ADDR);
    Wire.write(STEP_10 | bbb);
    Wire.write(STEP_1 | aaaa);
    Wire.endTransmission();
}

byte calculateVolume(int level) {
    if (level > 79)
        level = 79;
    if (level < 0)
        level = 0;
    level = 79 - level;     // invert the volume, so 0 is low and 79 is high
    uint8_t b = level / 10; // get the most significant digit (eg. 79 gets 7)
    uint8_t a = level % 10; // get the least significant digit (eg. 79 gets 9)
    b = b & 0b0000111;      // limit the most significant digit to 3 bit (7)
    return (b << 4) | a;    // return both numbers in one byte (0BBBAAAA)
}

void unmute() {
    byte value = MUTE_OFF;
    Wire.beginTransmission(EVC_ADDR);
    Wire.write(value);
    byte error = Wire.endTransmission();
}


Now the webpage looks like this on my phone:



Here is the complete code. I had to remove a few thing and optimize a bit as this barely fits in the memory of the Arduino.


/*
Simple web server to serve a form to turn on or off digital pins. In this case,
the pins are connected to relays to turn speakers on or off. This uses pins
2 through 9 in pairs, so 2 and 3 control the hot tub speakers, 4 and 5 control
the dining room speakers, and 6 and 7 control the portico speakers. 8 and 9 are
available in case I ever find a need to connect one more pair of speakers.
*/

#include <Ethernet.h>
#include <SPI.h>
#include <Wire.h>
#include <string.h>

// MAC address can be anything that is unique within the local network.
byte mac[] = {0x00, 0x1E, 0x2A, 0x76, 0x24, 0x08};

// Some unused IP address on the local network.
byte ip[] = {192, 168, 2, 251};

// web server, nothing fancy, just port 80 for http
EthernetServer server(80);

#define RELAY_ON 0
#define RELAY_OFF 1

// settings for the volume controller
#define EVC_ADDR 0x44       // volume control chip address
#define STEP_1 0b11010000   // 2-Channel, -1dB/step
#define STEP_10 0b11100000  // 2-Channel, -10dB/step
#define MUTE_OFF 0b01111000 // 2-Channel MUTE off

// true, just show speaker status or false, actually change the speaker states
boolean showStatus = false;

// which zones are on or off, initially, all are off
boolean zone1 = false;
boolean zone2 = false;
boolean zone3 = false;
boolean zone4 = false;

// default volume is 25 to start
int volume = 0;

// pin definition, one pin per speaker,
// so the left speaker in zone 1 is pin 2, etc
static const int zone1L = 2;
static const int zone1R = 3;
static const int zone2L = 4;
static const int zone2R = 5;
static const int zone3L = 6;
static const int zone3R = 7;
static const int zone4L = 8;
static const int zone4R = 9;

// set up pins, initially all speakers in all zones are off
void setupPins() {
    digitalWrite(zone1L, RELAY_OFF);
    digitalWrite(zone1R, RELAY_OFF);
    digitalWrite(zone2L, RELAY_OFF);
    digitalWrite(zone2R, RELAY_OFF);
    digitalWrite(zone3L, RELAY_OFF);
    digitalWrite(zone3R, RELAY_OFF);
    digitalWrite(zone4L, RELAY_OFF);
    digitalWrite(zone4R, RELAY_OFF);

    pinMode(zone1L, OUTPUT);
    pinMode(zone1R, OUTPUT);
    pinMode(zone2L, OUTPUT);
    pinMode(zone2R, OUTPUT);
    pinMode(zone3L, OUTPUT);
    pinMode(zone3R, OUTPUT);
    pinMode(zone4L, OUTPUT);
    pinMode(zone4R, OUTPUT);

    delay(2000);
}

void setup() {
    Ethernet.begin(mac, ip);
    server.begin();
    Wire.begin(); // join i2c bus
    setupPins();
    //Serial.begin(115200);
}

// set up buffer for reading web requests
static const int bufferMax = 128;
int bufferSize;
char buffer[bufferMax];

void loop() {
    EthernetClient client = server.available();
    if (client) {
        waitForRequest(client);
        handleRequest();
        client.stop();
    }
}

void waitForRequest(EthernetClient client) {
    bufferSize = 0;

    while (client.connected()) {
        if (client.available()) {
            char c = client.read();
            if (c == '\n') {
                break;
            } else {
                if (bufferSize < bufferMax) {
                    buffer[bufferSize++] = c;
                } else {
                    break;
                }
            }
        }
    }
}

void handleRequest() {
    // Received buffer contains a standard HTTP GET line, something like
    // "GET /?X=X&Y=Y HTTP/1.1".
    // Could have up to 4 parameters, 1=on&2=on&3=on&4=on,
    // one for each set of speakers. All that is necessary here is to
    // extract the query string and check for each of the zone numbers.
    String request = String(buffer);
    //int firstSpace = request.indexOf(" ");                // right after GET
    //int lastSpace = request.indexOf(" ", firstSpace + 1); // just after the query string
    //request = request.substring(firstSpace, lastSpace);
    //Serial.println(request);

    showStatus = request.indexOf("?") == -1;

    if (request.indexOf("1=on") > -1) {
        zone1 = true;
    } else if (request.indexOf("1=off") > -1) {
        zone1 = false;
    }
    if (request.indexOf("2=on") > -1) {
        zone2 = true;
    } else if (request.indexOf("2=off") > -1) {
        zone2 = false;
    }
    if (request.indexOf("3=on") > -1) {
        zone3 = true;
    } else if (request.indexOf("3=off") > -1) {
        zone3 = false;
    }
    if (request.indexOf("4=on") > -1) {
        zone4 = true;
    } else if (request.indexOf("4=off") > -1) {
        zone4 = false;
    }
    if (request.indexOf("volume=") > -1) {
        int start = request.indexOf("volume=") + 7; // 7 is length of "volume="
        int end = request.indexOf(" ", start);
        volume = request.substring(start, end).toInt();
    } else {
        if (request.indexOf("mute") > -1) {
            volume = 0;
        }
    }
    //Serial.println("Volume = " + String(volume));
    if (!showStatus) {
        setSpeakerState();
    }
    sendPage();
}

void setSpeakerState() {
    digitalWrite(zone1L, zone1 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone1R, zone1 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone2L, zone2 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone2R, zone2 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone3L, zone3 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone3R, zone3 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone4L, zone4 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone4R, zone4 ? RELAY_ON : RELAY_OFF);
    unmute();
    setVolume(volume);
}

void sendPage() {
    // http response header
    /*
    int length = 1442;
    if (digitalRead(zone4L) == RELAY_ON)
        length += 18;
    if (digitalRead(zone3L) == RELAY_ON)
        length += 18;
    if (digitalRead(zone2L) == RELAY_ON)
        length += 18;
    if (digitalRead(zone1L) == RELAY_ON)
        length += 18;
    */
    server.println("HTTP/1.1 200 OK");
    server.println("Content-Type: text/html");
    // server.print("Content-Length: ");
    // server.println(length);
    server.println();

    // html head
    server.println("<html><head><meta charset=\"UTF-8\">");
    server.println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
    server.println("<link rel=\"stylesheet\" href=\"http://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css\">");
    server.println("<title>Speaker Control</title>");
    server.println("<script>");
    server.println("function doit(){");
    server.println("document.getElementById(\"vF\").submit();}");
    server.println("</script></head><body>");

    // html body
    server.println("<div class='container'><h2>Speaker Control</h2><form action='/' role='form'>");

    int pinState = digitalRead(zone4L);
    printCheckBox(4, pinState, "Hot tub");
    

    pinState = digitalRead(zone3L);
    printCheckBox(3, pinState, "Dining Room");

    pinState = digitalRead(zone2L);
    printCheckBox(2, pinState, "Portico");
    
    /* not used
    pinState = digitalRead(zone1L);
    printCheckBox(1, pinState, "(Empty)");
    */

    server.print("<br/><button type='submit'>Save</button></form></div>");

    server.print("</div><div class='container'><h3>Master Volume</h3><form id='vF' action='/' role='form'>");
    server.print("<input type='range' name='volume' min='0' max='79' value='");
    server.print(volume);
    server.print("' oninput='doit(value)'>");
    server.print("</form>");
    server.print("<form id='mF' action='/' role='form'>");
    server.print("<input type='submit' name='mute' value='Mute'/></form></div></body></html>");
}

void printCheckBox(int zone, int pinState, String label) {
    server.print("<br/><input type='hidden' name='");
    server.print(zone);
    server.print("' value='off'/><div class='checkbox'><label><input type='checkbox' name='");
    server.print(zone);
    server.print("' value='on'");
    server.print(pinState == RELAY_ON ? " checked='checked'" : "");
    server.print(">");
    server.print(label);
    server.print("</label></div>");
}

void setVolume(int level) {
    byte bbbaaaa = calculateVolume(level);
    byte aaaa = bbbaaaa & 0b00001111;
    byte bbb = (bbbaaaa >> 4) & 0b00001111;

    Wire.beginTransmission(EVC_ADDR);
    Wire.write(STEP_10 | bbb);
    Wire.write(STEP_1 | aaaa);
    Wire.endTransmission();
    //Serial.println("set volume to " + (79 - level));
}

byte calculateVolume(int level) {
    if (level > 79)
        level = 79;
    if (level < 0)
        level = 0;
    level = 79 - level;     // invert the volume, so 0 is low and 79 is high
    uint8_t b = level / 10; // get the most significant digit (eg. 79 gets 7)
    uint8_t a = level % 10; // get the least significant digit (eg. 79 gets 9)
    b = b & 0b0000111;      // limit the most significant digit to 3 bit (7)
    return (b << 4) | a;    // return both numbers in one byte (0BBBAAAA)
}

void unmute() {
    byte value = MUTE_OFF;
    Wire.beginTransmission(EVC_ADDR);
    Wire.write(value);
    byte error = Wire.endTransmission();
    //if (error)
    //    Serial.println("mute, error code = " + String(error));
}