Thursday, February 2, 2023

Node-RED resources

Node-RED resources

Node-RED is a tool for easily connecting devices, APIs, and online services to automate tasks, create dashboards, and control your environment.


A little background will help you understand the concepts being used in Node-RED and automation in general.


Node-RED is a drag-and-drop based programming tool, originally developed as a side project in 2013 by Nick O’Leary and Dave Conway-Jones of IBM’s Emerging Technology Services group and it is now a part of the OpenJS Foundation.

You create Node-RED programs by dragging different "nodes" from a palette of available functions and then connect these nodes together. Each node has a well defined purpose; it does something with it's input and passes the result onto the next node(s) in the program.

Node-RED started out as a tool for visualizing and manipulating mapping between MQTT topics. It has grown far beyond that in the decade or so since it was invented. You can read more about Node-RED if you are interested.


MQTT (Message Queuing Telemetry Transport) was created by Dr. Andy Stanford-Clark and Arlen Nipper in 1999. The original purpose for MQTT was to allow remote monitoring devices used in the oil and gas industry to send their data to servers over expensive satellite connections.

MQTT is a lightweight machine-to-machine (M2M) messaging protocol for clients that are resource constrained (need a small code and data footprint) and are connected to unreliable networks or networks with limited or expensive bandwidth resources.

MQTT use a pub-sub (publish / subscribe) architecture. There are two basic components, clients and brokers. Clients connect to brokers and can publish data, subscribe to topics, or both.

MQTT messages contain a topic and a payload. Both the topic and payload are application defined.

The topic describes what the data is in a hierarchical format, like the directory structure on a computer.  The '/' character is used to separate each level. Some examples might be home/livingroom/temperaturegarage/temperature, or cabin/bedroom/humidity.

The payload is the data. Since the payload is application defined it could be an image from a camera, a sensor value (temperature, humidity, brightness level, on, off, etc.), a command ('on', 'off', 'dim', 'bright', etc.), or a collection of information (name: 'Jenny', phone: '888-867-5309', email: ''), audio data, and so on. Anything that can be digitized can be sent as a payload in MQTT.

This ~5 minute video will give you a brief intro to MQTT if you want to know more.

MQTT Broker (Mosquitto)

Mosquitto is an open source MQTT broker. Versions are available for a variety of platforms (windows, mac, linux, raspberry pi, etc.). They also host a test instance of their broker that you can use to test clients or just to get familiar with MQTT. They also have clients available for different platforms so you can subscribe to topics or publish messages.

RTL-SDR Software defined radios

More and more utilities are moving to "smart meters" that allow reading information without having to physically access the meter. Orem city just moved to smart water meters last year. And Rocky Mountain Power has been using smart power meters for several years.

The site has a number of tools for reading and decoding data from smart meters, tire pressure sensors in cars, weather sensors and a host of other RF devices. It's a simple matter to start reading and recording your power or water usage.

Water readings

Orem's water meters are easy to read via the rtlamr tool using the SCM+ protocol. You just need to open your meter box and look for the number printed in very large text on the side of your device. Put that number into the following command in place of xxxxx and you'll start seeing data within a minute or two.

rtlamr -format json -msgtype scm+ -filterid xxxxx -agcmode true 

The meter transmits data once a minute. Depending on the location of your antenna you may get all the messages or only a few of them. Note that even though it sends a message once a minute, the actual meter reading is only updated hourly (unfortunately). Also, the meter reading appears to be in 10ths of a gallon. In other words a value of 10 represents one gallon, not ten gallons.

Power readings

Reading your power meter should work very similarly using the rtlamr tool. I have a net meter so I don't have any direct experience here unfortunately (see caveat below). At the top of the power meter, above the display, is a sticker with some FCC info on it and a big long number. That's the device ID you need to filter on.

I tried reading a meter on a local utility pedestal. I can see the meter and get readings from it in rtlamr, but the readings don't seem to change. Over a three day period, the "consumption" value remained constant even though the actual display on the meter changed.

One caveat - Rocky Mountain Power seems to have moved their customers with rooftop solar to the new Itron Gen5 Riva meters. These seem to be configured in 2.4GHz mesh mode so you can no longer read the data with the usual rtlamr 900MHz tools. This seems to only apply to net metering solar customers, not non-solar customers.



The main Node-RED site:
Node-RED Flows
Node-RED for amateur radio group: nodered-hamradio
Mosquitto MQTT broker:

RTL-SDR - Software defined radio

RTL-SDR software defined radio news and projects
Quick guide to setup a RTL SDR Server on a Raspberry Pi.

International space station

🛰 International Space Station announcement when it is about to be overhead (and visible!)

Additional information


You can copy and "import" these to your node-RED instance. Hit the hamburger menu on the top right, then select "import" from the menu. Copy and paste the text below (without the title) into the box & hit import.

Air quality flow

[{"id":"7d0550b4d1c16c24","type":"inject","z":"28053dfef12b07e3","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"600","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":100,"wires":[["b64ff284222e38f2"]]},{"id":"b64ff284222e38f2","type":"http request","z":"28053dfef12b07e3","name":"AQI Lindon","method":"GET","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":310,"y":100,"wires":[["d79841da3c713e8b"]]},{"id":"0cb95d9d5e681a42","type":"debug","z":"28053dfef12b07e3","name":"AQI debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":470,"y":40,"wires":[]},{"id":"d79841da3c713e8b","type":"csv","z":"28053dfef12b07e3","name":"Parse CSV","sep":"\\t","hdrin":true,"hdrout":"none","multi":"mult","ret":"\\n","temp":"","skip":"0","strings":true,"include_empty_strings":"","include_null_values":"","x":490,"y":100,"wires":[["91791c876ea24429"]]},{"id":"91791c876ea24429","type":"change","z":"28053dfef12b07e3","name":"Latest","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[0]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":130,"y":180,"wires":[["e75db06bac7d35b1","0cb95d9d5e681a42"]]},{"id":"e75db06bac7d35b1","type":"split","z":"28053dfef12b07e3","name":"Split object","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"topic","x":290,"y":180,"wires":[["acdc02d4916b47b1"]]},{"id":"428713f7463a26d0","type":"comment","z":"28053dfef12b07e3","name":"AQI information","info":"Data from\n\n\nAccording to the\nfollowing ranges are applicable\n\n| AQI    | PM 2.5     | Ozone       |\n Good      0-12         0-0.054\n Moderate  12.1-35.4    0.055-0.070\n Sensitive 35.5-55.4    0.071-0.085\n Unhealthy 55.5-150.4   0.086-0.105\n Very bad  150.5-250.4  0.106-0.200\n Hazardous 250.5+       0.201+\n \n PM2.5 is in ug/m3 and based on a 24 hour average\n Ozone is in ppm and based on an 8 hour average\n\n NO2 levels from EPA\n\n\nPPB (state reports PPM)\nGood 0-50\nModerate 51-100\nSensitive 101-150\nUnhealth 151-200\nVery bad 201-300\n","x":140,"y":40,"wires":[]},{"id":"acdc02d4916b47b1","type":"switch","z":"28053dfef12b07e3","name":"Route messages","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"dtstamp","vt":"str"},{"t":"eq","v":"ozone","vt":"str"},{"t":"eq","v":"ozone_8hr_avg","vt":"str"},{"t":"eq","v":"pm25","vt":"str"},{"t":"eq","v":"pm25_24hr_avg","vt":"str"},{"t":"eq","v":"no2","vt":"str"},{"t":"eq","v":"co","vt":"str"},{"t":"eq","v":"temp","vt":"str"},{"t":"eq","v":"rh","vt":"str"},{"t":"eq","v":"ws","vt":"str"},{"t":"eq","v":"wd","vt":"str"},{"t":"eq","v":"sr","vt":"str"}],"checkall":"true","repair":false,"outputs":12,"x":170,"y":360,"wires":[["13a54c24be136349"],["c04efbac895729f3","2dfac0064328f31a"],["2dfac0064328f31a"],["d6162e12d5bf363b","73bb8cc649668118"],["73bb8cc649668118"],["fddc52cea250d9b6","d5b05f209645a903"],["90e52970c8438acc","1351cc1cf525fa04"],["9817090a6398563a","77f2c5f9b0041a8b"],["cfbd698e4fc69cbf","5784aae85b3353f1"],["9d44a12ce2fb9447"],["e93c3b9d67b7542f"],["1ebb09c7ebb423fd","3f1c7077656ad8b9"]],"inputLabels":["AQI messages"],"outputLabels":["dtstamp","ozone","ozone_8hr_avg","pm25","pm25_24hr_avg","no2","co","temp","rh","ws","wd","sr"]},{"id":"13a54c24be136349","type":"ui_text","z":"28053dfef12b07e3","group":"f178535d7c428ccf","order":1,"width":0,"height":0,"name":"time","label":"Time","format":"{{msg.payload}}","layout":"row-left","className":"","x":570,"y":200,"wires":[]},{"id":"c04efbac895729f3","type":"ui_gauge","z":"28053dfef12b07e3","name":"Ozone","group":"f178535d7c428ccf","order":2,"width":"3","height":"3","gtype":"gage","title":"Ozone","label":"PPM","format":"{{value}}","min":0,"max":"0.2","colors":["#00b500","#e6e600","#ca3838"],"seg1":"0.055","seg2":"0.086","diff":false,"className":"","x":570,"y":240,"wires":[]},{"id":"2dfac0064328f31a","type":"ui_chart","z":"28053dfef12b07e3","name":"Ozone","group":"f178535d7c428ccf","order":3,"width":"3","height":"3","label":"Ozone","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"12","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":690,"y":260,"wires":[[]]},{"id":"d6162e12d5bf363b","type":"ui_gauge","z":"28053dfef12b07e3","name":"PM2.5","group":"f178535d7c428ccf","order":4,"width":"3","height":"3","gtype":"gage","title":"PM2.5","label":"ug/m3","format":"{{value}}","min":0,"max":"250","colors":["#00b500","#e6e600","#ca3838"],"seg1":"12","seg2":"55.5","diff":false,"className":"","x":570,"y":280,"wires":[]},{"id":"73bb8cc649668118","type":"ui_chart","z":"28053dfef12b07e3","name":"PM2.5","group":"f178535d7c428ccf","order":5,"width":"3","height":"3","label":"PM2.5","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"12","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":690,"y":300,"wires":[[]]},{"id":"fddc52cea250d9b6","type":"ui_gauge","z":"28053dfef12b07e3","name":"NO2","group":"f178535d7c428ccf","order":6,"width":"3","height":"3","gtype":"gage","title":"NO2","label":"PPM","format":"{{value}}","min":0,"max":"0.3","colors":["#00b500","#e6e600","#ca3838"],"seg1":"0.051","seg2":"0.151","diff":false,"className":"","x":570,"y":320,"wires":[]},{"id":"d5b05f209645a903","type":"ui_chart","z":"28053dfef12b07e3","name":"NO2","group":"f178535d7c428ccf","order":7,"width":"3","height":"3","label":"NO2","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"12","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":690,"y":340,"wires":[[]]},{"id":"9817090a6398563a","type":"ui_gauge","z":"28053dfef12b07e3","name":"Temp","group":"2d8b6f8f77619143","order":1,"width":"3","height":"3","gtype":"gage","title":"Temp","label":"F","format":"{{value}}","min":"-10","max":"110","colors":["#00ffee","#00eb1b","#ca3838"],"seg1":"60","seg2":"90","diff":false,"className":"","x":570,"y":440,"wires":[]},{"id":"77f2c5f9b0041a8b","type":"ui_chart","z":"28053dfef12b07e3","name":"Temp","group":"2d8b6f8f77619143","order":2,"width":"3","height":"3","label":"Temp","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"12","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":690,"y":460,"wires":[[]]},{"id":"cfbd698e4fc69cbf","type":"ui_gauge","z":"28053dfef12b07e3","name":"Humid","group":"2d8b6f8f77619143","order":3,"width":"3","height":"3","gtype":"gage","title":"Humid","label":"%rh","format":"{{value}}","min":"0","max":"100","colors":["#00ff2a","#fbff00","#ca3838"],"seg1":"30","seg2":"50","diff":false,"className":"","x":570,"y":480,"wires":[]},{"id":"5784aae85b3353f1","type":"ui_chart","z":"28053dfef12b07e3","name":"Humid","group":"2d8b6f8f77619143","order":4,"width":"3","height":"3","label":"Humid","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"12","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":690,"y":500,"wires":[[]]},{"id":"9d44a12ce2fb9447","type":"ui_gauge","z":"28053dfef12b07e3","name":"Wind","group":"2d8b6f8f77619143","order":5,"width":"3","height":"3","gtype":"gage","title":"Wind","label":"MPH","format":"{{value}}","min":"0","max":"75","colors":["#00ff2a","#fbff00","#ca3838"],"seg1":"30","seg2":"50","diff":false,"className":"","x":570,"y":520,"wires":[]},{"id":"e93c3b9d67b7542f","type":"ui_gauge","z":"28053dfef12b07e3","name":"Dir","group":"2d8b6f8f77619143","order":6,"width":"3","height":"3","gtype":"compass","title":"Dir","label":"","format":"{{value}}","min":"0","max":"75","colors":["#00ff2a","#fbff00","#ca3838"],"seg1":"30","seg2":"50","diff":false,"className":"","x":690,"y":540,"wires":[]},{"id":"90e52970c8438acc","type":"ui_gauge","z":"28053dfef12b07e3","name":"CO","group":"f178535d7c428ccf","order":8,"width":"3","height":"3","gtype":"gage","title":"CO","label":"PPM","format":"{{value}}","min":0,"max":"1","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":570,"y":360,"wires":[]},{"id":"1351cc1cf525fa04","type":"ui_chart","z":"28053dfef12b07e3","name":"CO","group":"f178535d7c428ccf","order":9,"width":"3","height":"3","label":"CO","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"12","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":690,"y":380,"wires":[[]]},{"id":"1ebb09c7ebb423fd","type":"ui_gauge","z":"28053dfef12b07e3","name":"Solar","group":"2d8b6f8f77619143","order":9,"width":"3","height":"3","gtype":"gage","title":"Solar","label":"w/m2","format":"{{value}}","min":"0","max":"500","colors":["#00ff2a","#fbff00","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":570,"y":560,"wires":[]},{"id":"3f1c7077656ad8b9","type":"ui_chart","z":"28053dfef12b07e3","name":"Solar","group":"2d8b6f8f77619143","order":10,"width":"3","height":"3","label":"Solar","chartType":"line","legend":"false","xformat":"HH:mm","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"12","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":690,"y":580,"wires":[[]]},{"id":"f178535d7c428ccf","type":"ui_group","name":"Lindon","tab":"9d16aed7861d445a","order":3,"disp":true,"width":"6","collapse":true,"className":""},{"id":"2d8b6f8f77619143","type":"ui_group","name":"Weather","tab":"9d16aed7861d445a","order":2,"disp":true,"width":"6","collapse":true,"className":""},{"id":"9d16aed7861d445a","type":"ui_tab","name":"Air Quality","icon":"wi-darksky-wind","order":1,"disabled":false,"hidden":false}]


[{"id":"1da6e3bdacaca0f9","type":"worldmap","z":"9a8c4b547b38e156","name":"ISS map","lat":"","lon":"","zoom":"3","layer":"EsriS","cluster":"","maxage":"90","usermenu":"show","layers":"show","panit":"true","panlock":"false","zoomlock":"false","hiderightclick":"true","coords":"utm","showgrid":"true","showruler":"true","allowFileDrop":"false","path":"/iss","overlist":"RA,DN,TL","maplist":"OSMG,OSMC,EsriS","mapname":"","mapurl":"","mapopt":"","mapwms":false,"x":300,"y":280,"wires":[]},{"id":"cd15dbc58a3a4e01","type":"http request","z":"9a8c4b547b38e156","name":"Get ISS position","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","senderr":false,"x":300,"y":120,"wires":[["01020efb56ca3c3e"]]},{"id":"3fec0a7643a12c76","type":"inject","z":"9a8c4b547b38e156","name":"Every minute","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":120,"wires":[["cd15dbc58a3a4e01"]]},{"id":"01020efb56ca3c3e","type":"change","z":"9a8c4b547b38e156","name":"Convert to worldmap","rules":[{"t":"set","p":"","pt":"msg","to":"payload.iss_position.latitude","tot":"msg"},{"t":"set","p":"payload.lon","pt":"msg","to":"payload.iss_position.longitude","tot":"msg"},{"t":"set","p":"","pt":"msg","to":"International Space Station","tot":"str"},{"t":"set","p":"payload.icon","pt":"msg","to":"iss","tot":"str"},{"t":"set","p":"payload.iconColor","pt":"msg","to":"red","tot":"str"},{"t":"set","p":"payload.label","pt":"msg","to":"ISS","tot":"str"},{"t":"set","p":"payload.layer","pt":"msg","to":"ISS","tot":"str"},{"t":"set","p":"payload.weblink","pt":"msg","to":"[{\"name\":\"Next pass\",\"url\":\"\",\"target\":\"_new\"},{\"name\":\"Mission info\",\"url\":\"\",\"target\":\"_new\"}]","tot":"json"},{"t":"delete","p":"payload.iss_position","pt":"msg"},{"t":"delete","p":"payload.message","pt":"msg"},{"t":"delete","p":"payload.timestamp","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":300,"y":200,"wires":[["5cf81bf789afe7be","1da6e3bdacaca0f9"]]},{"id":"212a437e52c23cb1","type":"comment","z":"9a8c4b547b38e156","name":"ISS info & resources","info":"Next time ISS is overhead from NASA\n\n\nTracking the ISS with Node-RED on a Raspberry Pi\n\n\nISS announcement when it is about to be overhead (and visible!)\n\n","x":110,"y":40,"wires":[]},{"id":"5cf81bf789afe7be","type":"worldmap-tracks","z":"9a8c4b547b38e156","name":"ISS track","depth":"30","layer":"combined","smooth":true,"x":100,"y":280,"wires":[["1da6e3bdacaca0f9"]]}]

Ham DB lookup with map

[{"id":"5543b7dc63ef1bca","type":"ui_text_input","z":"134851ca88c24fe4","name":"Call sign","label":"Callsign","tooltip":"Enter the station call sign","group":"d9b5c411da0c8d24","order":0,"width":0,"height":0,"passthru":false,"mode":"text","delay":"0","topic":"callsign","sendOnBlur":false,"className":"","topicType":"str","x":100,"y":180,"wires":[["4dd7ff27f9fbf929","30acc081a38c741a"]]},{"id":"3d493dacd1cdafba","type":"http request","z":"134851ca88c24fe4","name":"hamdb lookup","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":480,"y":180,"wires":[["3c9e94c07a6eace1","379a1c3a2cb8c3ec"]]},{"id":"09d31ff412eb9d94","type":"worldmap","z":"134851ca88c24fe4","name":"Ham lookup","lat":"40.2605215","lon":"-111.7005643","zoom":"11","layer":"OSMC","cluster":"","maxage":"90","usermenu":"show","layers":"show","panit":"true","panlock":"false","zoomlock":"false","hiderightclick":"false","coords":"utm","showgrid":"true","showruler":"true","allowFileDrop":"false","path":"/ham","overlist":"RA,DN,TL","maplist":"OSMG,OSMC,EsriS","mapname":"","mapurl":"","mapopt":"","mapwms":false,"x":550,"y":380,"wires":[]},{"id":"95478a0270885a54","type":"change","z":"134851ca88c24fe4","name":"Convert to worldmap","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.hamdb.callsign","tot":"msg","dc":true},{"t":"set","p":"payload.label","pt":"msg","to":"","tot":"msg"},{"t":"set","p":"","pt":"msg","to":"$ & \": \" & $.payload.fname & \" \" & $.payload.mi & \" \" & $ & \" \" & $.payload.suffix","tot":"jsonata"},{"t":"set","p":"payload.icon","pt":"msg","to":"fa-user-circle-o","tot":"str"},{"t":"set","p":"payload.weblink","pt":"msg","to":"{\"name\":\"QRZ\",\"url\":\"\" & $ & \"/\",\"target\":\"_new\"}","tot":"jsonata"},{"t":"set","p":"payload.layer","pt":"msg","to":"hams","tot":"str"},{"t":"delete","p":"payload.fname","pt":"msg"},{"t":"delete","p":"payload.mi","pt":"msg"},{"t":"delete","p":"payload.suffix","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":300,"wires":[["09d31ff412eb9d94","23caf401.9d6d4c","7602a9da7abfc7d7"]]},{"id":"4dd7ff27f9fbf929","type":"template","z":"134851ca88c24fe4","name":"Convert to API","field":"url","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{{payload}}/json/uvarcdemo","output":"str","x":280,"y":180,"wires":[["3d493dacd1cdafba"]]},{"id":"379a1c3a2cb8c3ec","type":"change","z":"134851ca88c24fe4","name":"blank string","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":150,"y":60,"wires":[["0a7906eba955ab06"]]},{"id":"4deeb220be58cf5a","type":"ui_toast","z":"134851ca88c24fe4","position":"top right","displayTime":"3","highlight":"","sendall":true,"outputs":0,"ok":"OK","cancel":"","raw":false,"className":"","topic":"HamDB","name":"toast","x":750,"y":300,"wires":[]},{"id":"7602a9da7abfc7d7","type":"template","z":"134851ca88c24fe4","name":"Format toast","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{{}}","output":"str","x":570,"y":300,"wires":[["4deeb220be58cf5a"]]},{"id":"86b1f299b0b23707","type":"ui_template","z":"134851ca88c24fe4","group":"1671379f30926081","name":"hamqsl Band conditions","order":1,"width":"9","height":"4","format":"<center>\n    <a href=\"\"\n        title=\"Click to add Solar-Terrestrial Data to your website!\" target=\"_new\"><img src=\"\"></a>\n</center>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","className":"","x":330,"y":560,"wires":[[]]},{"id":"6a245d48aeabd4c3","type":"ui_template","z":"134851ca88c24fe4","group":"1671379f30926081","name":"RigRef Band conditions","order":2,"width":"9","height":"3","format":"<center><a href=\"\"\n target=\"_blank\"><img src=\"\" border=\"0\"></a></center>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","className":"","x":330,"y":600,"wires":[[]]},{"id":"7db57965.76d468","type":"ui_button","z":"134851ca88c24fe4","name":"Open ISS tab","group":"93eb817c13fb862d","order":1,"width":"3","height":"1","passthru":true,"label":"ISS","tooltip":"Open the ISS tab","color":"","bgcolor":"","className":"","icon":"satellite","payload":"iss","payloadType":"str","topic":"change-tab","topicType":"str","x":360,"y":500,"wires":[["6d6a371c653a1e38"]]},{"id":"5e1f0081.6dc3c","type":"ui_template","z":"134851ca88c24fe4","group":"93eb817c13fb862d","name":"window redirect","order":3,"width":0,"height":0,"format":"<script>\n(function(scope) {\n    scope.$watch('msg.payload', function(data) {\n      if (data == \"empty\" || data == \"\" || data == \"0\" || data === undefined)\n        return;\n\n\"http://localhost:1880/\" + data, data + \"_tab\");\n      /*\n      if (data == \"Node-RED\") {\n\"\", \"nodered_tab\");\n        //window.location.href = \"\";\n      } \n      if (data == \"GitHub\") {\n\"\", \"github_tab\");\n        //window.location.href = \"\";\n      } \n      */\n    });\n})(scope);\n</script>","storeOutMessages":false,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","className":"","x":720,"y":480,"wires":[[]]},{"id":"23caf401.9d6d4c","type":"ui_button","z":"134851ca88c24fe4","name":"Open ham tab","group":"93eb817c13fb862d","order":2,"width":"3","height":"1","passthru":true,"label":"Ham DB","tooltip":"Open the HAM DB tab","color":"","bgcolor":"","className":"","icon":"fa-id-card-o","payload":"ham","payloadType":"str","topic":"change-tab","topicType":"str","x":360,"y":460,"wires":[["6d6a371c653a1e38"]]},{"id":"6d6a371c653a1e38","type":"trigger","z":"134851ca88c24fe4","name":"reset","op1":"","op2":"","op1type":"pay","op2type":"str","duration":"250","extend":false,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":550,"y":480,"wires":[["5e1f0081.6dc3c"]],"info":"This is necessary or the window code won't move to the same window twice in a row"},{"id":"3c9e94c07a6eace1","type":"switch","z":"134851ca88c24fe4","name":"Success or fail","property":"payload.hamdb.messages.status","propertyType":"msg","rules":[{"t":"eq","v":"OK","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":700,"y":180,"wires":[["95478a0270885a54"],["fc65050e80308d0a"]],"outputLabels":["success","fail"]},{"id":"fc65050e80308d0a","type":"change","z":"134851ca88c24fe4","name":"fail - show status","rules":[{"t":"set","p":"payload","pt":"msg","to":"$flowContext('callsign') & \" \" & $.payload.hamdb.messages.status","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":240,"wires":[["4deeb220be58cf5a"]]},{"id":"0a7906eba955ab06","type":"delay","z":"134851ca88c24fe4","name":"delay .25 sec","pauseType":"delay","timeout":"250","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":330,"y":60,"wires":[["5543b7dc63ef1bca"]]},{"id":"30acc081a38c741a","type":"change","z":"134851ca88c24fe4","name":"Save call sign","rules":[{"t":"set","p":"callsign","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":280,"y":220,"wires":[[]]},{"id":"d9b5c411da0c8d24","type":"ui_group","name":"HamDB","tab":"5af3bc9c93f141e7","order":2,"disp":true,"width":"6","collapse":true,"className":""},{"id":"1671379f30926081","type":"ui_group","name":"Band conditions","tab":"5af3bc9c93f141e7","order":3,"disp":true,"width":"9","collapse":true,"className":""},{"id":"93eb817c13fb862d","type":"ui_group","name":"Default","tab":"5af3bc9c93f141e7","order":1,"disp":true,"width":"6","collapse":true,"className":""},{"id":"5af3bc9c93f141e7","type":"ui_tab","name":"Home","icon":"dashboard","order":2,"disabled":false,"hidden":false}] 


Thursday, June 23, 2022

Update the default list of search engines in Chrome

Update the default list of search engines in Chrome

Chrome allows you to manage the list of searches available via the address bar. You can add new site searches and such by clicking the Add button. What you can't do is move a search engine from the "Site search" section to the "Search engines" section. You are effectively stuck with the list that Google curated. Here's how to fix that.

Side note: The data we want to edit is stored in a SQLite database. You'll need a copy of the SQLite command line tool for your OS in order to edit this database. Good news, SQLite is free software! Just run on over to the SQLite home page at and hit the download button.

In your browser go to chrome://settings/searchEngines

Click the "Add" button (highlighted above) to add your new search engine. You could also search the list of engines and see if it's already there (use the search box in the top right, also highlighted above).

Once you've added or activated your desired search engine, you need to EXIT chrome. That includes all instances that might be running. I don't just mean close the settings tab, I mean completely exit all running instances of Chrome. This is necessary because the file storing this information is locked when Chrome is running.

Now we need to edit the database storing this info. Go to your Chrome profile directory. In Windows 10 it is located in the C:\Users\%UserName%\AppData\Local\Google\Chrome\User Data\Default directory in the "Web Data" file.

>cd "\Users\%UserName%\AppData\Local\Google\Chrome\User Data\Default"
>sqlite3 "Web Data"
sqlite> .mode line
sqlite> select * from keywords where url like '%brave%'
                     id = 632
             short_name = Brave
                keyword = b
            favicon_url =
                    url ={searchTerms}
   safe_for_autoreplace = 0
        originating_url =
           date_created = 13300482199622719
            usage_count = 0
        input_encodings =
            suggest_url =
         prepopulate_id = 0
      created_by_policy = 0
          last_modified = 13300482199622719
              sync_guid = 680f8753-590d-4cc6-8be5-ca0154aa3f2a
         alternate_urls = []
              image_url =
 search_url_post_params =
suggest_url_post_params =
  image_url_post_params =
            new_tab_url =
           last_visited = 0
  created_from_play_api = 0
              is_active = 1
        starter_pack_id = 0
sqlite> update keywords set prepopulate_id=id where url like '%brave%';
sqlite> .q

In this case I added the Brave search engine to my list of default search engines by updating the "prepopulate_id" field to a non-zero value. To search for your preferred engine, substitute the domain or some other unique part for the word brave in the queries above. Note that you need to keep the single quotes and the percent signs. The single quote tells SQLite that this is a string value. The percent signs are wildcards for characters before and after the keyword you are looking for.

You could also remove search engines from the default list by, you guessed it, setting the prepopulate_id to zero.

sqlite> update keywords set prepopulate_id=0 where url like '%ecosia%';

Now start Chrome again and your selected search engines should show up in the list of default search engines.

Thursday, July 29, 2021

CDC guidance isn't internally consistent


CDC guidance isn't internally consistent

Yesterday the CDC released new guidance on masks. The new rules call for everyone to wear masks whether vaccinated or not. This is based on secret data that the CDC has thus far refused to release (citing "CDC COVID-19 Response Team, unpublished data, 2021"). They've promised this data "real soon now" but thus far they have not been forthcoming with this crucial information.

Let's leave aside the lack of any supporting data for the moment and just focus on the policy. So the original theory was that the "vaccine" (see more about why it's not really a vaccine below) would allow us to "return to normal". Now, however, the CDC cryptically says that those who are vaccinated are still catching and transmitting the virus. Thus, everyone needs to wear masks again.

The CDC wants everyone to get vaccinated but they acknowledge that this won't prevent you from getting or transmitting the virus. So how, exactly, will everyone getting vaccinated help? Their "logic" defies ... well ... logic if we're honest about it.

Since the vaccine doesn't stop the virus we want everyone, including those who are vaccinated, to return to wearing masks. And since the virus isn't stopped by the vaccine, the only logical conclusion is that we will all be wearing masks forever.

But let's not forget, if masks worked we would have crushed this virus the last time we went through this charade. If masks stopped the virus, we wouldn't need an untested, experimental vaccine.

The "vaccine" wont stop the virus (according to CDC's unpublished data). Wearing masks also wont stop the virus (as we've already proven for the past year). Where does that leave us?

The real answer is what medical professionals said from the beginning. We need actual heard immunity. And that comes from a majority of the population getting the disease (not a vaccine that doesn't provide immunity or masks that don't slow or stop the spread).

Why it isn't a "vaccine"

Vaccine (noun): a substance used to stimulate the production of antibodies and provide immunity against one or several diseases, prepared from the causative agent of a disease, its products, or a synthetic substitute, treated to act as an antigen without inducing the disease.
Definition of "vaccine" from Oxford Languages

Vaccines, by definition, confer immunity (think MMR, DTAP, etc.). As we've seen, that's not the case with this "vaccine". Rather the COVID vaccines are more properly classified as therapeutics since all they actually do is reduce symptom severity. This is the same benefit provided by other therapeutics such as Hydroxychloroquine and Ivermectin.

Saturday, November 3, 2018

Soldering Resources

This article was originally written in 2018. It was updated with new prices and information in 2022.

Articles and Information

The following articles include basic information about how to solder and what tools you need. Many of these include videos and links to other resources.


Note that if you use the links below, I may get an Amazon affiliate commission. This in no way influences my recommendations.

Technically only 2 things are required to solder, a soldering iron and solder (there are even some soldering strips that you can activate with a match, but that's getting a bit out there). So let's talk briefly about solder then soldering irons. After that we'll look at some clamping options to hold things in place and then some other tools you might want to consider.


Solder comes in basically two varieties, rosin core and acid core. Acid core solder is for plumbing, NOT for electronics. DO NOT use acid core solder for electronics! Pretty much any rosin core solder will work just fine. I personally prefer lead-free solder in a small plastic tube (rather than on a roll). While slightly more expense, it is much easier to work with, and easier to carry for field work. You should avoid breathing solder fumes no matter what kind of solder you use, but this is especially important if you buy the (generally) less expensive tin-lead solder.

Soldering Irons

Irons can range from the cheap (sub $10) versions that plug directly into the wall up to thousands of dollars for high end soldering and rework stations. If you are just getting started, you don't need to spend a fortune to get a reasonable piece of equipment. At the end of this section are some soldering options for emergency and field use.

Save yourself some frustration and don’t buy a cheaper soldering iron from Radio Shack or Harbor Freight. Especially if you are just starting out, at least invest in a descent tool.  For $32 you can get the Velleman VTSS5U adjustable temperature soldering station. It has an analog temperature control with no display so you don’t know exactly where it is set, but at least it’s adjustable.

Hakko FX-888D digital soldering station ($106) is an excellent tool for hobbyists. This is the iron I'd recommend if you have any intention of soldering more than one or two small projects. This kit comes with a pair of the best cutters you’ll ever use. I love my Hakko cutters. I have several of them and I use them all the time.

For emergency / field use (only) there are some alternative soldering irons:


If you plan to solder much at all, you are going to want some way to clamp things in place while you solder them. This is true whether you are soldering wires together, soldering parts on a board, or soldering wires to a connector. Again, it doesn't have to be super expensive to be effective. Start with putty and clothespins and then move up to a vice if you find you are doing more soldering.

Plasti-Tak (poster putty, adhesive putty, mounting putty, reusable putty) is a great and cheap way to hold parts to boards, hold boards still, or hold wires and or connectors in place while soldering. The cheapest you can find should work just fine (Elmers $6.50, or Loctite $2). Try this video if you want to see it in action (“Blu-Tack” is the same as poster putty, Plasti-Tak, etc.). Some people use Silly Putty instead. That works too but I prefer the extra stick and stiffness of the poster putty.

Wooden (not plastic; they melt) clothespins make a great inexpensive clamp for holding wires, boards, or small parts when soldering.  You can cut the angled ends off to make a ‘flush’ or ‘flat’ face to your “clamp” (see picture above). Combine clothespins with reusable putty (previous tip) and you have a very portable, inexpensive, and versatile clamping system.  This guy made a fixture to hold the clothespins in place. You could do the same with a piece of scrap wood (1x6, etc.) for essentially free.

The Stick Vise ($35) is a great little portable vice for soldering. Takes almost no space and you can 3D print your own jaws if you are so inclined.

There are some 3D printable clamps you might consider as well.


In addition to clamps, there are a number of other tools you might consider. Three of the most useful are wire cutters, pliers, and a soldering mat.


As I mentioned previously, I love my Hakko CHP-170 micro cutters ($9). They are relatively inexpensive, and worth every penny. I have several pair that I keep stashed around so a pair is always within easy reach.

Hakko long nose pliers ($15) are just a pleasure to use. The jaws close nice and flat and they hold parts very nicely.

A silicone mat ($8) is one of the best investments you can make. They are very inexpensive but make it much easier to work. It helps you keep track of parts and, unlike your kitchen table, you can’t burn the mat with your soldering iron.


A fume extractor / air filter ($26) is a great piece of safety equipment, especially if you use lead based solder. The fumes given off during soldering are toxic and should be avoided. You should also clean the table or other areas where you soldered with wet wipes to remove any dust or debris.

As with most projects, safety glasses ($2) are also recommended. Solder can pop and spatter, especially when desoldering or soldering wires.

Sunday, September 16, 2018

Arbitrary depth recursive queries in SQL

Sometimes the data in your SQL database represents hierarchical data. For example, a bill of materials, departments, or reporting structures (employee directory).

You may not know it, but you can actually do fully recursive queries directly in SQL using common table expressions (CTEs).  I wrote up a github 'gist' including example data and SQL code to demonstrate how to do this.

I'm using SQLite in this example but any SQL language that implements the WITH keyword should be able to do the same thing. If you've never used SQLite before, you are missing out on an amazing, cross-platform, open source, single-file, self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world. I encourage you to check it out.

Check out my post about recursive SQL queries here on github

Friday, April 1, 2016

Makeblock mCore Information


mCore board
The great folks over at Makeblock have created a nice little board for creating robots, the mCore.

The mCore is basically an Arduino Uno plus
  • dual motor controller
  • two serial RGB LEDs (WS2812 aka "NeoPixels")
  • piezo buzzer
  • light sensor
  • IR LED
  • IR receiver
  • button
  • header block for either a bluetooth or 2.4GHz radio
  • four RJ25 connectors for external peripherals

The board is intended to be used for teaching programming, robotics, internet of things, etc.  As a learning tool, it is primarily used with the custom version of the Scratch programming environment called mBlock.  There is also a library for regular Arduino IDE programming as well.

What is lacking, however, is a good write up on the various pin assignments used for the on-board peripherals that Makeblock added.  Thankfully, at least there is a schematic to help us out.

Pin assignments

Based on the schematic, here are the pin assignments.  Additional detail on each peripheral is provided below the table.

Arduino mCore pin assignments
D0/RXD RXD on external radio connector
D1/TXD TXD on external radio connector
D2 IR receiver input
D3~ IR LED output (HIGH = ON)
D4 M2 direction (HIGH = CCW)
D5~ M2 PWM (speed)
D6~ M1 PWM (speed)
D7 M1 direction (HIGH = CW)
D8 Buzzer output
D9~ Pin 5 on RJ25 #2
D10~ (SPI) SS Pin 6 on RJ25 #2
D11~ (SPI) MOSI Pin 5 on RJ25 #1
D12 (SPI) MISO Pin 6 on RJ25 #1
D13 (SPI) SCLK Blue LED / Serial out to WS2812 LEDs
A0 Pin 5 on RJ25 #4
A1 Pin 6 on RJ25 #4
A2 Pin 5 on RJ25 #3
A3 Pin 6 on RJ25 #3
A4 (I2C) SDA Pin 2 on all 4 RJ25 connectors
A5 (I2C) SCL Pin 1 on all 4 RJ25 connectors
A6 Light sensor input
A7 Button input, low when pressed
Arduino Pin mCore function

Motor Controller

Motor Controller
The Toshiba TB6612 is used as a dual channel motor controller.  The inputs to the controller for each motor are a PWM pin for speed and two direction pins.  The mCore designers simplified the interface such that a single Arduino digital pin controls direction (it is inverted in hardware to provide the second direction input to the motor controller) while a second PWM pin controls speed.

The M1 connector on the board is controlled by D6 (PWM speed) and D7 (direction).  When D7 is HIGH, the motor turns CW.  When D7 is LOW the motor turns CCW.

Likewise, the M2 connector is controlled by D5 (PWM speed) and D4 (direction).  However, in this case the function of D4 is reversed. When D4 is HIGH the motor turns CCW, and when it is LOW it turns CW.

Reversing the function of the two direction pins may seem odd, however, when you think about it, the motors are generally on the opposite sides of the robot so they need to turn opposite directions for the bot to move either forward or backward.  Consequently you can use the same 'direction' setting (HIGH or LOW) on both outputs to get the bot to go in one direction, for example, forward.

Serial RGB LEDs (WS2812 aka NeoPixels)

WS2812 Serial RGB LEDs
There are two WS2812 RGB LEDs on the board.  These share a pin (digital pin 13) with the traditional LED seen on most Arduino boards.  You can use any of the existing NeoPixel libraries to control these LEDs.

In addition, the pin (D13) controls a blue LED on the board so the standard Arduino blink sketch will work correctly on the mCore.

Piezo Buzzer / Speaker

 The speaker is connected to digital pin 8.  You can drive this pin using the standard Arduino tone() library call.

Light Sensor

The light sensor is a standard CdS photocell configured as a voltage divider.  This is hooked to analog pin 5 so you can read the voltage (and hence the amount of light) using analogRead().


The IR LED is simply hooked to a digital output pin (digital pin 3). You can use this to transmit data or to detect objects in front of the bot (by bouncing light off of objects and reading it with the receiver).

IR Receiver

IR Receiver
The IR receiver, on digital pin 2, is intended to be used with the included remote control. It can also be used in conjunction with the IR LED for communications between bots.  The Makeblock library includes routines to read the button presses from the remote.


This is just a basic pushbutton with a pull-up resistor hooked to analog pin 7.  When the button is pressed, the value on A7 will be low.  It can be used to start your program or pretty much any other function you program in.  Unfortunately the designers wasted an analog input with just one button.  They could have included more buttons on the same pin by simply creating a voltage divider network with each of the buttons having a different resistor (and thus a different voltage / value when reading A7).

Radio Headers

Radio headers
The bot comes with either a bluetooth or 2.4GHz radio. This is the connector for the radio.  The design allows the radio to communicate using the standard Arduino Serial() calls.

RJ25 Connectors

RJ25 Connectors
The four RJ25 connectors make it easy to connect a variety of peripherals to the mCore. The top of each connector has a color coded block to indicate the types of peripheral that can be attached.  In turn, the peripherals themselves have color coded connectors.  Hooking things up is as easy as matching the colors.
Each connector has power (+5) and ground connections along with the I2C bus clock and data pins.  The remaining two pins on each connector have either two digital or two analog port pins, depending on which connector it is.
Port 1 has digital pins 11 (PWM) & 12.  Port 2 has digital pins 9 & 10 (both PWM).  Port 3 has analog pins 2 & 3.  Port 4 has analog pins 0 and 1.

Thursday, January 14, 2016

Internet sales tax

Internet sales tax just makes no sense.  It is simply another version of interstate sales tax which also makes no sense.  The simple solution is to pay sales tax where an item is sold (it is called "Sales Tax" after all) and be done with it.

Let's look at the edge cases which are often instructive in revealing issues and problems.

1. Let's say you are traveling outside your home state and you have a flat tire.  You buy a new tire, paying sales tax in the jurisdiction where you purchase the tire, and drive home.  Do you now owe additional tax to your home state for that product (the tire) you brought into the state?  If so, haven't you been double taxed?

2. Now let's look at the exact same transaction slightly differently.  What if I live in another state and I'm now in the process of moving to my new state. During the trip, while still in my previous state of residence, I have a flat tire.  I buy a new tire in my old state and then drive across the line into my new state.  Do I owe sales tax to my new state of residence?  How is this any different than the example above?  What if I bought new tires a day (or a week, a month, a year) before I moved?  Does that change the answer?

3. What if I buy a burger at a fast food restaurant just over the state line on my way home?  Do I owe tax to my home state when I get back?  Does it matter if I consume the burger before or after I cross the state line?  What if I stay out of state for several days (thus the food is consumed, processed, and completely 'used' out of state)?

4. Do I owe sales tax to my home state on gasoline that I purchase during a trip to another state? What if I bought that gas just before crossing the line into my home state?  What if all of the gas is used in the other state?  What if only 1/2 the gas is used outside the state; do I owe sales tax on only the 1/2 I brought back?  What if that gas is only used in a rental car that stays entirely in the other state?

5. What if, during a car trip in another state, I have my oil changed and I bring that new oil back into my home state (in my crankcase)?  Would it be any different if I bought a case of oil in another state and brought it home to change my own oil?  Do I have to pay sales tax to my home state on both the oil and the service or only on the oil portion of the transaction?  What if my home state doesn't tax services, only tangible products?  What if I drove 2000 miles (of the 3000 before I need another oil change) out of state; would I only owe on 1/3 of the cost?

6. What if I purchase a service in another state (say a massage or a car wash)?  Do I owe tax on the service that was entirely performed in the other state?  What if my state doesn't tax services?

7. Let's say my home state doesn't tax a particular class of item (food for example).  If I buy that item in another state do I still owe sales tax to my home state for the out-of-state transaction? If the other state does tax that class of item am I owed a refund from my home state (turn about is fair play, right)?

8. Compare this to adjacent cities with different tax rates.  Do I owe sales tax to my home city if I buy something in the next town over?

There are numerous other edge cases as I'm sure you can see.  These cases make it clear that interstate sales tax just makes no sense whatsoever.  There are an equal number of edge cases that apply to internet sales tax which is really no different.

In addition, there is no way to get 'credit' for the taxes you did pay at the point of sale.  Thus, any interstate taxes you pay constitute double taxation which is also unfair.

The argument made by proponents of this scheme is that the transaction takes place "at" the user's browser & therefore in the user's home state. If that's true, doesn't every internet merchant need a business license in each city? That's only fair, right? They have a sales presence in each home, right? Of course that's just ridiculous. As ridiculous as internet sales tax.

The basic concept of sales tax is to pay for the costs of doing business in that area (infrastructure, police, fire, building inspector, etc.).  Since you are using none of those infrastructure bits in your home state when you buy something out of state, it makes no sense to pay tax to your home state. Further, the state, county, & city that actually did pay for the infrastructure supporting your transaction got paid nothing. Not to mention that your home county & city get no tax revenue, just the state.