Unfinished Bitness

Finishing bits of technology, vintage and new.

  • About
  • Contact

Splunking Apple Music Library

Posted by Ripdubski on 2021.02.21
Posted in: Apple, Audio. Tagged: Analysis, Apple Music, iTunes, Splunk. Leave a comment

Splunk is a fantastic data analytics tool.  I’ve been involved with it for the past few years at work.  Splunk allows you to download and run it freely with a limited ingest, which is perfect for home use and/or training.  I have it setup at home, primarily to learn, and sometimes to try new things.

Warning, this is a long post.

In the past I wrote a Javascript application called iStats, which I posted about a couple of times.  iStats was designed to provide play and library analytics for my iTunes (now Apple Music) library.  I was interested in seeing most played tracks, artists, and albums, and other statistics.  The previous blog posts for iStats are here:

Original: https://unfinishedbitness.info/2018/05/04/itunes-missing-feature-library-statistics/

Update: https://unfinishedbitness.info/2020/12/06/itunes-missing-feature-library-statistics-revised/

While iStats does exactly what I need, I was curious how Splunk could provide any additional insights as it has a far greater capability to analyze big data.  My Apple Music Library is an 18MB XML file when exported, with roughly 9,000 songs.  This post will demonstrate the searches and dashboard I built in Splunk to provide analytics on my library.  There are 22 searches with corresponding visual reports built into a single dashboard.

 

iStats Results

iStats produces output like this.  In the most recent update (linked above) I added the ability to export each track to CSV so I could pull the data into Elastic, another tool similar to Splunk.  I did create a dashboard in Elastic, but didn’t make the effort to blog about it.  The only comment I will make in comparing the two is that it was much easier to create the Splunk dashboard.

 

Splunk Dashboard

First, let’s see the Splunk dashboard.  It’s broken into 5 parts.  Open the images to see a larger version.  Each section has a title that describes what it represents:

 

Getting Data In To Splunk

To get the data into Splunk, I used the CSV export from my iStats program.  Note there is only one major section of the exported Apple Music library XML file that should be transposed into the CSV file – there is other stuff in the XML you don’t want to be part of the analysis.  Without iStats, you may not be able to fully replicate what I’ve done here with accurate results. If you don’t want to use iStats, fine, however you will need a similar CSV.  The required fields in the CSV are (the ones in quotes are alpha and quoted):

"Artist","Album","Track","Year","Genre",Plays,Skips,Size,Minutes,Seconds,Milliseconds,"Bitrate","Kind"

An example of the data:

"Candlebox","Candlebox","Far Behind","1993","Hard Rock",14,0,36579379,4,59,666,"971","Apple Lossless audio file"

The first step is to export the Apple Music library.  To export the library from Apple Music, from the File menu, use the Library menu item, and the Export Library… sub menu item.

The second step is to transform the exported XML file to CSV, omitting the items that are not a targeted part of the analysis.  This included movies/videos, voice memos, playlists, etc.  The target is only the songs.  iStats extracts only what is needed from the XML.  If you don’t use iStats, your mileage may vary.  Open iStats, ensure export to CSV is checked, select the exported XML file, and wait a few seconds.  Once the results are displayed, the CSV file should be saved as well.

Now that the CSV is created, it can be pulled into Splunk.  You’ll need to be logged into Splunk as a user with permissions to upload data.  If “Add Data” doesn’t show up, your user doesn’t have the proper permissions.  So, select Add Data:

 

Next, select Upload:

 

Next, select the CSV file.  This can be done using the picker or drag and drop:

 

Next, Splunk doesn’t understand how to define the timestamp, so it gets set to current.  This will mark all records with the current date and time:

 

Next, set the destination index.  If you want to use the searches I provide later, create and name the index “applemusic” (no spaces):

 

Next, review the selections, and click Submit:

 

Lastly, Splunk will show a summary and provide some options for exploring the newly imported data.  You can select “Start Searching”:

 

Going to the Search app will allow you to see the raw data:

 

Building the Dashboard

When building the dashboard, you do not need the elevated permissions required to import the data.  The single dashboard being built is called “Apple Music Statistics”.  It will have the description “Apple Music Library and Playtime Statistics”:

 

To build the dashboard, 22 searches will be created.  Note that I mis-spelled one here – it says “Aple” instead of “Apple”.  I didn’t notice until after capturing everything for this blog post.  Correct it as you go.  The searches are:

 

Building the Searches

I will build the searches in the order they appear on the dashboard as depicted at the top of this post (see Splunk Dashboard above).  Each of these will be created from the Search app.  Each search is listed with the Dashboard Section Name and the Search Name in parentheses:

Example: Dashboard Section Name (Search Name)

 

Library Kinds (Apple Music Kind)

index="applemusic" Kind!=Undefined Kind!="Purchased AAC audio file" 
| stats count by Kind 
| sort -count

 

Library Genres (Apple Music Genre)

index="applemusic" Kind!=Undefined Kind!="Purchased AAC audio file" 
| stats count by Genre 
| sort -count,Genre

 

Library Artist Count (Apple Music Library Artist Count)

index="applemusic" Artist != Compilation 
| dedup Artist 
| stats count

 

Library Album Count (Apple Music Library Album Count)

index="applemusic" 
| eval cd=Artist+" - "+Album
| dedup cd 
| stats count

 

Library Track Count (Apple Music Library Track Count)

index="applemusic" 
| stats count

 

Library Size in Bytes (Apple Music Library Size)

index="applemusic" 
| stats sum(Size) as s_size

 

Library Duration (Apple Music Library Duration)

index="applemusic" 
| stats sum(Minutes) as s_minutes sum(Seconds) as s_seconds sum(Milliseconds) as s_milliseconds 
| eval tot_seconds=(s_minutes*60)+s_seconds+floor(s_milliseconds/1000) 
| eval str_seconds=tostring(tot_seconds,"duration")
| rex field=str_seconds mode=sed "s/((((\d*)\+)?(\d*):)?(\d*):)?(\d*)/\4 days \5 hrs \6 mins \7 secs/"
| rex field=str_seconds mode=sed "s/^ days/0 days/"
| rename str_seconds as "Library Duration"
| table "Library Duration"

 

Playtime Duration (Apple Music Play Duration)

index="applemusic" 
| eval track_seconds=Plays*((Minutes*60)+Seconds+floor(Milliseconds/1000)) 
| stats sum(track_seconds) as play_seconds
| eval str_seconds=tostring(play_seconds,"duration")
| rex field=str_seconds mode=sed "s/((((\d*)\+)?(\d*):)?(\d*):)?(\d*)/\4 days \5 hrs \6 mins \7 secs/"
| rex field=str_seconds mode=sed "s/^ days/0 days/"
| rename str_seconds as "Play Duration"
| table "Play Duration"

 

Library Top 10 Artists by Track Count (Apple Music Library Artist Top 10)

index="applemusic" Artist != "Compilation" 
| top limit=10 Artist 
| rename count as Tracks 
| table Artist,Tracks

 

Library Top 10 Albums by Track Count (Apple Music Library Albums Top 10)

index="applemusic" 
| eval cd=Artist+" - "+Album 
| top 10 cd

 

Total Play Count (Apple Music Play Total)

index=applemusic 
| stats sum(Plays) as "Total Plays"

 

Total Skip Count (Apple Music Skip Total)

index=applemusic 
| stats sum(Skips) as "Total Skips"

 

Play Top 10 Artists (Apple Music Play Top 10 Artists)

index="applemusic" 
| stats sum(Plays) as "Play Count" by Artist 
| sort -"Play Count" 
| head 10 
| table Artist,"Play Count"

 

Play Top 10 Albums (Apple Music Play Top 10 Albums)

index="applemusic" 
| eval art_album=Artist + " - " + Album 
| stats sum(Plays) as "Play Count" by art_album 
| rename art_album as "Album Name" 
| sort -"Play Count" 
| head 10

 

Play Top 10 Tracks (Apple Music Play Top 10 Tracks)

index="applemusic" 
| sort -Plays 
| eval title = Artist + " - " + Track 
| table title,Plays 
| head 10

 

Skip Top 10 Artists (Apple Music Skip Top 10 Artists)

index="applemusic" 
| stats sum(Skips) as "Skip Count" by Artist 
| sort -"Skip Count" 
| head 10 
| table Artist,"Skip Count"

 

Skip Top 10 Albums (Apple Music Skip Top 10 Albums)

index="applemusic" 
| eval art_album=Artist + " - " + Album 
| stats sum(Skips) as "Skip Count" by art_album 
| rename art_album as "Album Name" 
| sort -"Skip Count" 
| head 10

 

Skip Top 10 Tracks (Apple Music Skip Top 10 Tracks)

index="applemusic" 
| sort -Skips 
| eval title = Artist + " - " + Track 
| table title,Skips 
| head 10

 

Library Top 10 Years (Apple Music Library Top 10 Years)

index="applemusic" 
| stats count as "Track Count" by Year 
| sort -"Track Count" 
| head 10

 

Play Top 10 Years (Apple Music Play Top 10 Years)

index="applemusic" 
| stats sum(Plays) as "Play Count" by Year 
| sort -"Play Count" 
| head 10

 

Library Top 10 Genres (Apple Music Library Top 10 Genre)

index="applemusic" 
| stats count as "Track Count" by Genre 
| sort -"Track Count" 
| head 10

 

Play Top 10 Genres (Apple Music Play Top 10 Genres)

index="applemusic" 
| stats sum(Plays) as "Play Count" by Genre 
| sort -"Play Count" 
| head 10

 

Dashboard Source

If your interested in the dashboard source XML, here it is.  You can see the colors set, chart options, etc.  Apologies the indenting was not preserved:

<dashboard theme="dark">
<label>Apple Music Statistics</label>
<description>Apple Music Library and Playtime Statistics</description>
<row>
<panel>
<chart>
<title>Library Kinds</title>
<search ref="Apple Music Kind"></search>
<option name="charting.drilldown">none</option>
</chart>
</panel>
<panel>
<chart>
<title>Library Genres</title>
<search ref="Apple Music Genre"></search>
<option name="charting.drilldown">none</option>
</chart>
</panel>
</row>
<row>
<panel>
<single>
<title>Library Artist Count</title>
<search>
<query>index="applemusic" Artist != Compilation | dedup Artist | stats count</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="colorBy">value</option>
<option name="colorMode">none</option>
<option name="drilldown">none</option>
<option name="numberPrecision">0</option>
<option name="rangeColors">["0x53a051","0x0877a6","0xf8be34","0xf1813f","0xdc4e41"]</option>
<option name="rangeValues">[0,30,70,100]</option>
<option name="showSparkline">1</option>
<option name="showTrendIndicator">1</option>
<option name="trellis.enabled">0</option>
<option name="trellis.scales.shared">1</option>
<option name="trellis.size">medium</option>
<option name="trendColorInterpretation">standard</option>
<option name="trendDisplayMode">absolute</option>
<option name="unitPosition">after</option>
<option name="useColors">0</option>
<option name="useThousandSeparators">1</option>
</single>
</panel>
<panel>
<single>
<title>Library Album Count</title>
<search ref="Apple Music Library Album Count"></search>
<option name="drilldown">none</option>
</single>
</panel>
<panel>
<single>
<title>Library Track Count</title>
<search>
<query>index="applemusic" | stats count</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="colorBy">value</option>
<option name="colorMode">none</option>
<option name="drilldown">none</option>
<option name="numberPrecision">0</option>
<option name="rangeColors">["0x53a051","0x0877a6","0xf8be34","0xf1813f","0xdc4e41"]</option>
<option name="rangeValues">[0,30,70,100]</option>
<option name="showSparkline">1</option>
<option name="showTrendIndicator">1</option>
<option name="trellis.enabled">0</option>
<option name="trellis.scales.shared">1</option>
<option name="trellis.size">medium</option>
<option name="trendColorInterpretation">standard</option>
<option name="trendDisplayMode">absolute</option>
<option name="unitPosition">after</option>
<option name="useColors">0</option>
<option name="useThousandSeparators">1</option>
</single>
</panel>
</row>
<row>
<panel>
<single>
<title>Library Size in Bytes</title>
<search>
<query>index="applemusic" 
| stats sum(Size) as s_size</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="colorBy">value</option>
<option name="colorMode">none</option>
<option name="drilldown">none</option>
<option name="numberPrecision">0</option>
<option name="rangeColors">["0x53a051", "0x0877a6", "0xf8be34", "0xf1813f", "0xdc4e41"]</option>
<option name="rangeValues">[0,30,70,100]</option>
<option name="showSparkline">1</option>
<option name="showTrendIndicator">1</option>
<option name="trellis.enabled">0</option>
<option name="trellis.scales.shared">1</option>
<option name="trellis.size">medium</option>
<option name="trendColorInterpretation">standard</option>
<option name="trendDisplayMode">absolute</option>
<option name="unitPosition">after</option>
<option name="useColors">0</option>
<option name="useThousandSeparators">1</option>
</single>
</panel>
</row>
<row>
<panel>
<single>
<title>Library Duration</title>
<search ref="Apple Music Library Duration"></search>
<option name="drilldown">none</option>
</single>
</panel>
<panel>
<single>
<title>Playtime Duration</title>
<search ref="Apple Music Play Duration"></search>
<option name="colorMode">none</option>
<option name="drilldown">none</option>
<option name="rangeColors">["0x53a051","0x0877a6","0xf8be34","0xf1813f","0xdc4e41"]</option>
<option name="useColors">0</option>
</single>
</panel>
</row>
<row>
<panel>
<table>
<title>Library Top 10 Artists by Track Count</title>
<search>
<query>index="applemusic" Artist != "Compilation" | top limit=10 Artist | rename count as Tracks | table Artist,Tracks</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="drilldown">none</option>
</table>
</panel>
<panel>
<table>
<title>Library Top 10 Albums by Track Count</title>
<search>
<query>index="applemusic" | eval cd=Artist+" - "+Album | top 10 cd | rename cd as "Album Name" | rename count as Tracks | table "Album Name",Tracks</query>
<earliest>0</earliest>
<latest></latest>
<sampleRatio>1</sampleRatio>
</search>
<option name="drilldown">none</option>
<option name="refresh.display">progressbar</option>
</table>
</panel>
</row>
<row>
<panel>
<single>
<title>Total Play Count</title>
<search>
<query>index=applemusic | stats sum(Plays) as "Total Plays"</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="colorMode">none</option>
<option name="drilldown">none</option>
<option name="rangeColors">["0x53a051","0x0877a6","0xf8be34","0xf1813f","0x53a051"]</option>
<option name="useColors">1</option>
</single>
</panel>
<panel>
<single>
<title>Total Skip Count</title>
<search>
<query>index=applemusic | stats sum(Skips) as "Total Skips"</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="drilldown">none</option>
<option name="rangeColors">["0x53a051","0x0877a6","0xf8be34","0xf1813f","0xdc4e41"]</option>
<option name="useColors">1</option>
</single>
</panel>
</row>
<row>
<panel>
<table>
<title>Play Top 10 Artists</title>
<search>
<query>index="applemusic" | stats sum(Plays) as "Play Count" by Artist | sort -"Play Count" | head 10 | table Artist,"Play Count"</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="count">20</option>
<option name="dataOverlayMode">none</option>
<option name="drilldown">none</option>
<option name="percentagesRow">false</option>
<option name="rowNumbers">false</option>
<option name="totalsRow">true</option>
<option name="wrap">true</option>
<format type="color" field="Play Count">
<colorPalette type="minMidMax" maxColor="#53A051" minColor="#FFFFFF"></colorPalette>
<scale type="minMidMax"></scale>
</format>
<format type="number" field="Play Count">
<option name="precision">0</option>
</format>
</table>
</panel>
<panel>
<table>
<title>Play Top 10 Albums</title>
<search>
<query>index="applemusic" | eval art_album=Artist + " - " + Album | stats sum(Plays) as "Play Count" by art_album | rename art_album as "Album Name" | sort -"Play Count" | head 10</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="count">20</option>
<option name="dataOverlayMode">none</option>
<option name="drilldown">none</option>
<option name="percentagesRow">false</option>
<option name="rowNumbers">false</option>
<option name="totalsRow">true</option>
<option name="wrap">true</option>
<format type="color" field="Play Count">
<colorPalette type="minMidMax" maxColor="#53A051" minColor="#FFFFFF"></colorPalette>
<scale type="minMidMax"></scale>
</format>
<format type="number" field="Play Count">
<option name="precision">0</option>
</format>
</table>
</panel>
<panel>
<table>
<title>Play Top 10 Tracks</title>
<search>
<query>index="applemusic" | sort -Plays | eval title = Artist + " - " + Track | table title,Plays | head 10</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="count">20</option>
<option name="dataOverlayMode">none</option>
<option name="drilldown">none</option>
<option name="percentagesRow">false</option>
<option name="rowNumbers">false</option>
<option name="totalsRow">true</option>
<option name="wrap">true</option>
<format type="color" field="Plays">
<colorPalette type="minMidMax" maxColor="#53A051" minColor="#FFFFFF"></colorPalette>
<scale type="minMidMax"></scale>
</format>
<format type="number" field="Plays">
<option name="precision">0</option>
</format>
</table>
</panel>
</row>
<row>
<panel>
<table>
<title>Skip Top 10 Artists</title>
<search>
<query>index="applemusic" | stats sum(Skips) as "Skip Count" by Artist | sort -"Skip Count" | head 10 | table Artist,"Skip Count"</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="count">20</option>
<option name="dataOverlayMode">none</option>
<option name="drilldown">none</option>
<option name="percentagesRow">false</option>
<option name="rowNumbers">false</option>
<option name="totalsRow">true</option>
<option name="wrap">true</option>
<format type="color" field="Skip Count">
<colorPalette type="minMidMax" maxColor="#DC4E41" minColor="#FFFFFF"></colorPalette>
<scale type="minMidMax"></scale>
</format>
<format type="number" field="Skip Count">
<option name="precision">0</option>
</format>
</table>
</panel>
<panel>
<table>
<title>Skip Top 10 Albums</title>
<search>
<query>index="applemusic" | eval art_album=Artist + " - " + Album | stats sum(Skips) as "Skip Count" by art_album | rename art_album as "Album Name" | sort -"Skip Count" | head 10</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="count">20</option>
<option name="dataOverlayMode">none</option>
<option name="drilldown">none</option>
<option name="percentagesRow">false</option>
<option name="rowNumbers">false</option>
<option name="totalsRow">true</option>
<option name="wrap">true</option>
<format type="color" field="Skip Count">
<colorPalette type="minMidMax" maxColor="#DC4E41" minColor="#FFFFFF"></colorPalette>
<scale type="minMidMax"></scale>
</format>
<format type="number" field="Skip Count">
<option name="precision">0</option>
</format>
</table>
</panel>
<panel>
<table>
<title>Skip Top 10 Tracks</title>
<search>
<query>index="applemusic" | sort -Skips | eval title = Artist + " - " + Track | table title,Skips | head 10</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="count">20</option>
<option name="dataOverlayMode">none</option>
<option name="drilldown">none</option>
<option name="percentagesRow">false</option>
<option name="rowNumbers">false</option>
<option name="totalsRow">true</option>
<option name="wrap">true</option>
<format type="color" field="Skips">
<colorPalette type="minMidMax" maxColor="#DC4E41" minColor="#FFFFFF"></colorPalette>
<scale type="minMidMax"></scale>
</format>
<format type="number" field="Skips">
<option name="precision">0</option>
</format>
</table>
</panel>
</row>
<row>
<panel>
<chart>
<title>Library Top 10 Years</title>
<search>
<query>index="applemusic" | stats count as "Track Count" by Year | sort -"Track Count" | head 10</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="charting.chart">column</option>
<option name="charting.chart.showDataLabels">all</option>
<option name="charting.drilldown">none</option>
</chart>
</panel>
<panel>
<chart>
<title>Play Top 10 Years</title>
<search>
<query>index="applemusic" | stats sum(Plays) as "Play Count" by Year | sort -"Play Count" | head 10</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="charting.chart">column</option>
<option name="charting.chart.showDataLabels">all</option>
<option name="charting.drilldown">none</option>
</chart>
</panel>
</row>
<row>
<panel>
<chart>
<title>Library Top 10 Genres</title>
<search>
<query>index="applemusic" | stats count as "Track Count" by Genre | sort -"Track Count" | head 10</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="charting.chart">column</option>
<option name="charting.chart.showDataLabels">all</option>
<option name="charting.drilldown">none</option>
</chart>
</panel>
<panel>
<chart>
<title>Play Top 10 Genres</title>
<search>
<query>index="applemusic" | stats sum(Plays) as "Play Count" by Genre | sort -"Play Count" | head 10</query>
<earliest>0</earliest>
<sampleRatio>1</sampleRatio>
</search>
<option name="charting.chart">column</option>
<option name="charting.chart.showDataLabels">all</option>
<option name="charting.drilldown">none</option>
</chart>
</panel>
</row>
</dashboard>

That’s it for now.  As I add more to the dashboard, I’ll post about the additions.

 

 

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

Action! String Padding

Posted by Ripdubski on 2021.02.13
Posted in: Atari, Programming. Tagged: 8 bit, Action!. Leave a comment

In the last post I showed a function to trim a string.  In this post I go the other way and demonstrate how to pad one.  This is useful when trying to print data in alignment.

The Function

This function was primarily written to left pad numbers.  As such there is a limit of 10 in length which is longer than the longest Action! number.  The code is again pretty well documented and will be including in the next release of my Action! library.  Here is short summary of how it works.

The procedure is called with 3 parameters.  The first is a pointer to a character array, which can not be a static string and must be a variable because the length will be altered.  The second parameter is the character you want to pad the string with.  Common values are space and zero.  The third parameter is the length to pad to (no more than 10).

First the procedure fills a temporary string with 10 of the desired pad character.  Next it copies the incoming string into the padded temporary string at the furthest right position to accommodate the length of the incoming string, essentially right justifying it.  Next the temporary padded string is copied back to the incoming string pointer, and the incoming string length is set to the specified pad length.

; --------------------------------------
; Proc..: StrPad(CHAR POINTER pS CHAR bc BYTE bL)
; Param.: pS=Pointer to string to pad
;         bC=Character to pad with
;         bL=Length to pad to
; Desc..: Left pads a string with a char
; Notes.: Max of 10 in length.
; --------------------------------------
PROC StrPad(CHAR POINTER pS CHAR bC BYTE bL)
; Declare a string filled with 10 spaces
CHAR ARRAY pA="          "

; Fill the temp string with desired char
; Use pA+1 which is the first character of the string.
; pA is the length of the string so skip over it.
SetBlock(pA+1, 10, bC)

; Copy incoming string into temp string
; Use desired length - incoming string length + 1 as position in temp string
; which puts incoming string at correct rightmost spot to accommodate its length.
SAssign(pA, pS, bL-pS(0)+1, bL)

; Copy newly padded temp string to incoming string
SCopy(pS, pA)

; Set incoming string pointer to new length
pS(0)=bL

RETURN

 

Sample Usage

Here is a short program demonstrating it usage.  Note that in this example the StrPad() function is not seen as its been incorporated into the LIBSTR.ACT file as part of my Action! library.  This will pad a byte, a card, and int to various lengths.

INCLUDE "D3:DEFINES.ACT"
INCLUDE "D3:LIBSTR.ACT"

PROC Main()
; Declare a byte, a card, an integer, and character array of 10 in length
BYTE bnum
CARD cnum
INT inum
CHAR ARRAY snum(10)

; Pad single character length byte to 2 positions with 0 character
bnum=1
; Convert the number into a string
StrB(bnum,snum)
; Call the pad function, passing the string, the pad character, and pad length
StrPad(snum, '0, 2)
; Show result
PrintF("Num: (2) [.....1]->[%S]%E",snum)

; Pad two character length byte to 4 positions with 0 character
bnum=21
; Convert the number into a string
StrB(bnum,snum)
; Call the pad function, passing the string, the pad character, and pad length
StrPad(snum, '0, 4)
; Show result
PrintF("Num: (4) [....21]->[%S]%E",snum)

; Pad five character length card to 5 positions with 0 character
cnum=64256
; Convert the number into a string
StrC(cnum,snum)
; Call the pad function, passing the string, the pad character, and pad length
StrPad(snum, '0, 5)
; Show result
PrintF("Num: (5) [.64256]->[%S]%E",snum)

; Pad five character length int to 8 positions with space character
inum=24768
; Convert the number into a string
StrI(inum,snum)
; Call the pad function, passing the string, the pad character, and pad length
StrPad(snum, 32, 8)
; Show result
PrintF("Num: (8) [.24768]->[%S]%E",snum)

; Pad six character length negative int to 8 positions with space character
inum=-24768
; Convert the number into a string
StrI(inum,snum)
; Call the pad function, passing the string, the pad character, and pad length
StrPad(snum, 32, 8)
; Show result
PrintF("Num: (8) [-24768]->[%S]%E",snum)

RETURN

 

Output

Here is the output showing that it works:

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

Action! String Trimming

Posted by Ripdubski on 2021.02.13
Posted in: Atari, Programming. Tagged: 8 bit, Action!. Leave a comment

This post will demonstrate a function to trim a string.  By trim, I mean to remove all trailing spaces and stop at the first non space character.

 

The Function

Here is the function, which I’ve added to my Action! library.  A new version will be released soon that will include recent additions and many fixes.  It’s pretty well documented, but here is a short summary.

The procedure is called by passing a reference to a character array in (the string you want to trim).  This procedure will modify it directly, so it can not be a static string and must be a variable.  The procedure will start by setting a counter to the length of the incoming string.  It will move from the end to the beginning by decreasing this counter.  It examines the character at the counters position in the string.  If it’s a space, it sets the the new string length to the counter position minus one (to eliminate the space).  If it’s not a space, the procedure exits having found a termination point.   Each iteration through the loop, the counter is decremented by one to move one character left.

; -------------------------------------- 
; Proc..: StrTrim(CHAR POINTER ps)
; Param.: pS=Pointer to string to trim
; Desc  : Trims space from string end
; -------------------------------------- 
PROC StrTrim(CHAR POINTER pS)
BYTE bL,bC

; Set counter to end of string 
bL=pS(Ø)

; Loop from string end to start 
while bL>=1
DO
  ; If char is space, set len to curr-1 
  if pS(bL)=32 then
    bC=bL-1 
  else
    ; Char is not space, exit loop
    exit
  fi

  ; Decrease counter
  bL==-1 
OD

; Set string pointer length to new value 
pS(Ø)=bC

RETURN

 

Sample Usage

Here is a sample program demonstrating its use.  Note that the function has been incorporated into the file LIBSTR.ACT so you don’t see it in this source:

; Include library file that has StrTrim() in it.
INCLUDE "D3:LIBSTR.ACT"

MODULE

PROC Main()
; Declare character array of 20 in length
CHAR ARRAY cA(20)

; Copy some content in the char array defining all 20 chars
SCopy(cA,"12345 abc !         ")

; Print it to show a valid padded definition
PrintF("B4 : [%S]%E",cA)

; Call the trim function passing the char array pointer
StrTrim(cA)

; Print it again to show its been trimmed
PrintF("Aft: [%S]%E",cA)

RETURN

 

Output

Here is the output from the program showing it works:

 

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

Action! DEFINE’ing

Posted by Ripdubski on 2021.02.12
Posted in: Atari, Programming. Tagged: 8 bit, Action!. Leave a comment

Over the course of my interactions with Action!, for whatever reason, I never had a need for defining a string value.  While numeric definitions are cake, string ones turned out not to be so much.

Let’s explore what I wanted to do.  I wanted to compare a variable that was input against a static string.  Why not just hard code the string?  Well, it is used in multiple locations, and having it DEFINEd makes it easy to change in all of them at once.  Thats the whole point of using DEFINE.  The source code I wanted to write:

if SCompare(sA, SSREADY) = 0 then

So using a define for SSREADY in an INCLUDEd source file accomplishes the goal of making it easy to change in just one place and use it multiple times across projects.  Something like this was needed for my code work:

DEFINE SSREADY = "TEST"

That works for numbers with no problem:

DEFINE NVAL = "100"

Anywhere that NVAL is found in the source, it will be replaced with 100.

So what is the issue?  Used in the context of SCompare above, it ends up producing code like this:

if SCompare(sA, TEST) = 0 then

Obviously thats not the intent of the source and it won’t work, and the compiler will tell you so immediately.  The compiler will think TEST is an undefined variable.  The word TEST needs double quotes around it.  Thats the goal.

Reading through the manual you will understand that in order to print double quotes, you have to precede each one with another.  Such as:

PrintE("Hello ""world""!")

which produces the output:

Hello "world"!

So moving back to the DEFINE statement it appears it needs to look like this:

DEFINE SSREADY = ""TEST""

But that doesn’t work either.  The compiler doesn’t like it.  The solution is to wrap the dual double quoted value in another set of double quotes.  But there is a caveat – they can not be all run together, or the compiler doesn’t understand which two of the 3 should be translated into a single double quote.  So you need to place a space between the DEFINE’s double quotes and the values double quote pair.  The proper syntax for DEFINEing a string value is:

DEFINE SSREADY = " ""TEST"" "

Here is a sample program that demonstrates this concept:

DEFINE NVAL = "100"
DEFINE SVAL = " ""TEST"" "

PROC Main()
PrintF("NVAL=[%B]%E",NVAL)
PrintF("SVAL=[%S]%E",SVAL)
RETURN

This will properly produce the output desired, and also allow for use in my original use case – SCompare().

Output:

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

FujiNet Time in Action!

Posted by Ripdubski on 2021.02.03
Posted in: Atari, Programming. Tagged: 8 bit, Action!, APETIME, FujINet. 1 Comment

FujiNet is a fantastic new device for the Atari 8 bit line of computers which provides WiFi, and SD card storage among other things, all over SIO.  One of the functions provided is APETIME support, a protocol developed by AtariMax as part of their Atari Peripheral Emulator (APE).

In the last post I demonstrated my BASIC version of a program to get the time from the FujiNet device using APETIME. The procedure is documented here (https://github.com/FujiNetWIFI/fujinet-platformio/wiki/Accessing-the-Real-Time-Clock).  At the time of this writing there was no Action! example.

After completing the BASIC version, I moved to creating an Action! version.  This post details my solution.  I wrote the routine into a procedure which can be called from the main program at any point.  It only requires passing a pointer to a  6 byte array.

After failing to get success using the described 5 byte assembly procedure on the FujiNet wiki by using an inline assembly code block, I instead created a procedure with the name SIOV and setting its address to the OS SIO vector address.

The routine that gets the time from FujiNet is the FNGTime() procedure (FujiNet Get Time).  It first sets the DCB variables with their addresses set to the DCB (Device Control Block) memory location.  This is convenient for assigning (poking) values into those locations, with no need for the Poke function.

Next it sets up the SIO call by assigning the appropriate values into the DCB.  It sets the buffer location (DBUF), to the address of the byte array passed in.

Last it calls the SIO vector using the SIOV procedure.

The main routine sets aside 6 bytes of storage, calls the FNGTime() routine with the address of the storage array, then prints the result in a friendly fashion.

 

; Prog..: FNTIME.ACT
; Author: Wade Ripkowski
; Date..: 2021.02.01
; Desc..: Gets date/time from FujiNet

MODULE

; -----------------------------------
; Proc..: SIOV()
; Desc..: OS SIO Vector
; -----------------------------------
PROC SIOV=$E459()

; -----------------------------------
; Proc..: FNGTime(BYTE ARRAY bA)
; Desc..: Get date/time from FujiNet
; via APETIME protocol
; Return: 6 bytes DMYHMS into bA
; -----------------------------------
PROC FNGTime(BYTE POINTER bA)
; Set DCB variable pointers
BYTE DDEVIC=$300,
DUNIT=$301,
DCOMND=$302,
DSTATS=$303,
DTIMLO=$306
CARD DBUF=$304,
DBYT=$308

; Setup SIO call using byte array address
; by putting values into the DCB.
; APETime=Device 69 ($45), Unit 1
; Time command=147 ($93)
; Get 6 bytes, timeout just over 15s
DDEVIC=69
DUNIT=1
DCOMND=147
DSTATS=64
DTIMLO=15
DBUF=bA
DBYT=6

; Call SIO
SIOV()

RETURN

; -----------------------------------
; Main Routine
; -----------------------------------
PROC Main()
; Storage for 6 bytes preset to 0
BYTE ARRAY bDT(6)=[0 0 0 0 0 0]

; Call time routine with the storage array
FNGTime(bDT)

; What time is it?
PrintF("Date: 20%B.%B.%B%E",bDT(2),bDT(1),bDT(0))
PrintF("Time: %B:%B:%B%E",bDT(3),bDT(4),bDT(5))

RETURN

 

I’m going to include the FNGTime() function in my Action! library which was developed in 2016 and  released in early 2017 right here on this blog (search posts with Action! tag), and is now at version 1.5 and will be re-released soon as an update.

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

FujiNet Time in BASIC

Posted by Ripdubski on 2021.02.03
Posted in: Atari, Programming. Tagged: 8 bit, APETIME, BASIC, FujINet. Leave a comment

FujiNet is a fantastic new device for the Atari 8 bit line of computers which provides WiFi, and SD card storage among other things, all over SIO.  One of the functions provided is APETIME support, a protocol developed by AtariMax as part of their Atari Peripheral Emulator (APE).

I wanted to write a function in Action! to get the time from the FujiNet device using APETIME, which is documented here (https://github.com/FujiNetWIFI/fujinet-platformio/wiki/Accessing-the-Real-Time-Clock). The procedure is straight forward and includes a BASIC example.  At the time of this writing there was no Action! example.

I examined the BASIC source and wasn’t happy with it’s length, specifically not liking the large IF block for the number padding.  I thought I could do something shorter and more efficient, and I wanted to fully understand the APETIME call before creating an Action! version.  In this post, I detail my BASIC program to get (and display) the date/time via APETIME from the FujiNet device.

In my version, I store the APETIME result in a string variable instead of page 6.  I also have eliminated the peek block by integrating those into a much shortened IF block that does inline 0 padding of the numbers.  For information on the Poke’s see the FujiNet wiki page (linked above).  Pokes for location 772 and 773 are the low and high bytes of the buffers string address in memory.

0 REM Program: FNTIME.BAS
1 REM Desc...: Gets time from FujiNet via APETIME using SIO
9 REM ----- Variables
10 DIM FNC$(5),BUF$(6),T$(2),YR$(4),MO$(2),DY$(2),HR$(2),MN$(2),SE$(2)
20 YR$="2000":MO$="00":DY$="00":HR$="00":MN$="00":SE$="00":LT=0
29 REM ----- Get address and MSB/LSB of string buffer
30 BA=ADR(BUF$):BH=INT(BA/256):BL=BA-(BH*256)
39 REM ----- Load assebly routine into function string
40 FOR I=1 TO 5:READ D:FNC$(I)=CHR$(D):NEXT I
99 REM ----- Setup SIO call for APETIME Get
100 POKE 768,69
105 POKE 769,1
110 POKE 770,147
115 POKE 771,64
120 POKE 772,BL:POKE 773,BH
125 POKE 774,15
130 POKE 776,6:POKE 777,0
199 REM ----- Execute assembly SIO call
200 X=USR(ADR(FNC$))
299 REM ----- Parse 6 byte result into strings
300 T$=STR$(PEEK(BA+2)):LT=LEN(T$)
305 YR$(5-LT)=T$
310 T$=STR$(PEEK(BA+1)):LT=LEN(T$)
315 MO$(3-LT)=T$
320 T$=STR$(PEEK(BA)):LT=LEN(T$)
325 DY$(3-LT)=T$
330 T$=STR$(PEEK(BA+3)):LT=LEN(T$)
335 HR$(3-LT)=T$
340 T$=STR$(PEEK(BA+4)):LT=LEN(T$)
345 MN$(3-LT)=T$
350 T$=STR$(PEEK(BA+5)):LT=LEN(T$)
355 SE$(3-LT)=T$
399 REM ----- Display results
400 ? "Date: ";YR$;".";MO$;".";DY$
410 ? "Time: ";HR$;":";MN$;":";SE$
990 END
999 REM ----- Data byte for assembly SIO jump
1000 DATA 104,32,89,228,96

In the next post I will show my Action! solution.

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

Atari SpartaDOS Disk Images – Revisited

Posted by Ripdubski on 2021.02.03
Posted in: Atari. Tagged: 720K, 8 bit, Atari800MacX, ATR, Disk Image, SpartaDOS. 1 Comment

SpartaDOS is great MS-DOS like DOS (Disk Operating System) for the Atari 8 bit line of computers.  It was released on disk and eventually on cartridge too.  It received updates to allow hard drive access and boasted one of the best memory footprints, which is critical for these old 48K to 128K machines.  It also supports sub directories and timestamps.  It is my favorite Atari based DOS.  It’s still actively maintained today!

Atari started out at 90K disks (single sided single density 5.25″ floppy).  By comparison, a double sided single density 3.5″ floppy is 720K.  720K was a size never achieved with stock hardware throughout Atari’s existence.  This is a lot more room to work with.

In a previous post here (https://unfinishedbitness.info/2014/04/19/atari-spartados-disk-images/) I posted about creating a 720K ATR disk image with SpartaDOS on it.  Over the years I found that the specific sector size and configuration is problematic for some utilities that manipulate/use ATR’s (outside of the emulator).  I also found a 360K format I was using that was also incompatible outside the emulator.  Note, they do work fine in the emulator.

In this post, I’m describing the required format specifications to achieve a fully compatible ATR image that is sized at 720K, as well as 360K, 180K, and 90K.

I’ll be referencing the Atari800MacX create new disk image utility, accessible from the Media menu:

90K Configuration

When creating the image in Atari800MacX, you can use the standard single density option, or choose Custom and set the sector count to 720 and sector size to 128 bytes.

Afterward using SpartaDOS X Format utility, select the drive number, set the density to “SINGLE”, tracks to 40 SS (single sided).  This should show a sector count of 720 and 128 BPS (bytes per sector), with a  total of 92,160 bytes.  Then use W rite Directory to “format” the disk.

Now you can use DIR to see the contents, which should be empty, and see the number of free sectors which will be slightly less than the 720.  Also use CHKDSK to verify the sector size and total capacity:

180K Configuration

When creating the image in Atari800MacX, you can use the standard double density option, or choose Custom and set the sector count to 720 and sector size to 256 bytes.

Afterward using SpartaDOS X Format utility, select the drive number, set the density to “DOUBLE”, tracks to 40 SS (single sided).  This should show a sector count of 720 and 256 BPS (bytes per sector), with a  total of 184,320 bytes.  Then use W rite Directory to “format” the disk.

Now you can use DIR to see the contents, which should be empty, and see the number of free sectors which will be slightly less than the 720.  Also use CHKDSK to verify the sector size and total capacity:

360K Configuration

There are two formats you can use for 360K images, and both will work. You need only decide which sector size you want based on needed efficiency.  Larger sectors will consume more space for many small files, but will work out better for a few large files.

Option A (512 byte sectors)

When creating the image in Atari800MacX, you need to choose Custom and set the sector count to 720 and sector size to 512 bytes.

Afterward using SpartaDOS X Format utility, select the drive number, set the density to “DD 512”, tracks to 40 DS (double sided).  This should show a sector count of 720 and 512 BPS (bytes per sector), with a  total of 368,640 bytes.  Then use W rite Directory to “format” the disk.

Now you can use DIR to see the contents, which should be empty, and see the number of free sectors which will be slightly less than the 720.  Also use CHKDSK to verify the sector size and total capacity:

Option B (256 byte sectors)

When creating the image in Atari800MacX, you need to choose Custom and set the sector count to 1440 and sector size to 256 bytes. This is more efficient for many small files.

Afterward using SpartaDOS X Format utility, select the drive number, set the density to “DD 512”, tracks to 40 DS (double sided).  This should show a sector count of 1440 and 256 BPS (bytes per sector), with a  total of 368,640 bytes.  Then use W rite Directory to “format” the disk.

Now you can use DIR to see the contents, which should be empty, and see the number of free sectors which will be slightly less than the 1440.  Also use CHKDSK to verify the sector size and total capacity:

720K Configuration

When creating the image in Atari800MacX, you need to choose Custom and set the sector count to 1440 and sector size to 512 bytes.

Afterward using SpartaDOS X Format utility, select the drive number, set the density to “DD 512”, tracks to 80 DS (double sided).  This should show a sector count of 1440 and 512 BPS (bytes per sector), with a  total of 737,280 bytes.  Then use W rite Directory to “format” the disk.

Now you can use DIR to see the contents, which should be empty, and see the number of free sectors which will be slightly less than the 1440.  Also use CHKDSK to verify the sector size and total capacity:

Thats it!

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

iTunes Missing Feature – Library Statistics – Revised

Posted by Ripdubski on 2020.12.06
Posted in: Programming. Tagged: Apple Music, CSS, HTML, iTunes, Javascript, Statistics. 2 Comments

A while back (2018) I created a utility to display playback statistics from iTunes, now Apple Music which I called iStats (https://unfinishedbitness.info/2018/05/04/itunes-missing-feature-library-statistics).  Apple deprecated the automatic creation of the music library XML file not long ago, and that diminished my enthusiasm and support for my iStats utility.  Recently I discovered you can generate a compatible XML file with Apple Music so I added few things to iStats and cleaned it up a bit.  Though no longer automatic, you can still create it on the fly using menu item File > Library > Export Library…

During the update, the tools and resources I used:

  • Microsoft VSCode. (https://code.visualstudio.com).  This is a new tool for me after having used Atom for many years (https://atom.io), which is a very nice editor in its own right.
  • Javascript – Interprested programming language that runs within web browser (https://en.wikipedia.org/wiki/JavaScript) (https://www.w3schools.com/Js/)
  • HTML5 – HyperText Markup Language (https://www.w3schools.com/html/html5_intro.asp)
  • CSS – Cascading Style Sheets (https://www.w3schools.com/Css/)
  • chart.js – Charting library (http://www.chartjs.org)
  • list.js – List library (http://listjs.com)
  • FileSaver.js – Local file saving library (https://github.com/eligrey/FileSaver.js/)

One of the first updates I made was to clean up the Javascript code and start the process of code re-use.  The main effort to date is making functions to set the chart colors.  This cut down quite a bit of redundant code and there is still more work to do.  I’ll work on more later.

The next updates I wanted to include were:

  • Ability to set number of ranked results in charts.  There was a static 10 defined before, now it will allow the selection from 1 to 10.
  • Ability to export the song library to CSV file.  This will allow import into big data analysis tools like Splunk (https://splunk.com) and Elasticsearch (https://elastic.co).  With these tools one can perform all kinds of analysis and reporting on the library.
  • Ability to metricize song duration and total listening time broken down in various terms.

The main page is pretty much the same, along with new version information:

 

The primary difference on the main page is the addition of two more input elements.  These two elements should be set before selecting the music library XML file in the third element since that one starts the report generation process:

 

The elements are self explanatory.

  • For the results number, choose from 1 to 10.  The default is 10.
  • For the export, simply check or uncheck.  The default is checked.  If checked, a CSV file called AppleMusicLibrary.csv will be created and saved to your browsers default save location.  Some browsers may ask for permission to do this.  The song durations in the CSV file are for the song itself, not the duration of listen time for the song.  Everything else exported in it should be self explanatory.

Before starting, open Apple Music and export the Library file in XML format.  Select File, then Library, then Export Library.  Give it a name and location of your choosing.

To begin report generation after selecting the report options, use the “Choose file” button to open a native file selector.  Locate and select the library XML file you previously exported:

Once selected, the application then processes the file, and updates the Document Object Model (DOM – https://www.w3schools.com/js/js_htmldom.asp) with the graph data.  You won’t see any screen updates.  This is due to how Javascript works within the browser.  Any changes in the DOM are rendered in the browser window when the Javascript completes.

At completion, iStats will try to save a file called “AppleMusicLibrary.csv” to your browsers default save location.  You may have to give it permission.

I’ll refer you to the previous iStats post (https://unfinishedbitness.info/2018/05/04/itunes-missing-feature-library-statistics) for general overview of the statistics generated and section output.  

With that said, there is a new section which will appear before the Results section.  This new section is Total Time Listened.  It displays the total combined duration all songs have been listened to accounting for play counts of each.  It will display one line showing the total days, hours, minutes, seconds, and milliseconds.  Following that will be alternative views for total seconds, minutes, and hours listened as single entities:

 

To use iStats, simply extract the linked zip file.  To make things easy, a copy of chart.js, list.js, and filesaver.js are included (distributed via MIT License), but you should download them yourself.  For your own sanity, review the Javascript source in the “iStats.html” file so you can see there is no funny business going on.  It reads the XML file, updates the html page DOM, and optionally saves the csv file, thats it.

iStats is released under the Simplified BSD License.  Feel free to use, modify and improve.

Link to the iStats zip file containing everything you need (hosted at Google Drive): iStats-Distro.zip

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

Asus WRT Router Icons

Posted by Ripdubski on 2020.09.12
Posted in: General, Router. Tagged: ASUSWRT. 1 Comment

Asus routers come with a handful of icons available to tag your devices.  This visual cue in the list of online devices can speed identification.  While there are a lot of Apple devices present, it is missing a few.  In addition it is missing some for home automation, connected devices, and security.

I wanted to have all the devices have icons that look like they were part of Asus WRT.  To start I grabbed a screenshot of the available device icons:

 

From this image I was able to find the fill color and icon color.  The fill color is #798D94, or RGB (121,141,148):

 

The border and drawing color is #2F383B , or RGB (47,56,59):

I then used Pixelmator Pro to create a transparent background image sized at 640 x 640 pixels.

From there a drew a box with rounded corners (150 px radius), with a stroke of 22 px solid line on the inside using the border/draw color of the Asus images, and a fill color matching the fill color of the Asus images.

I saved this as a template, and exported it as a PNG.  Basically it’s a blank icon:

 

Then I found icons I could use, and altered the color to match the border/draw color of the Asus images.  If the images were much smaller than my 640 pixel blank, I shrunk the blank so it was just bigger than the icon.  Once the blank and icon matched sizing, I copied the icon into the blank.  Then I shrank the image to 128 x 128 pixels, and exported it to PNG.

Now I have Apple TV and Apple Watch icons, and a few others.  Here are the ones I created:

 

I created several others as well including a garage door, scale, security, doorbell, thermostat, bed, nas drive, and voip phone.

The links on the images above should take you to the full size image should you want to save them and use them.

 

Enjoy.

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

DOS Batch Helper Redux, Part 3

Posted by Ripdubski on 2019.10.20
Posted in: MSDOS, Programming. Tagged: Batch, Menu. Leave a comment

The A-Batch code post.  As stated in the previous post, the code could definitely use some sanity checks on parameters. There is also room for some optimizations throughout, and removal of a couple of magic numbers. As it stands it works for what I need it to do.  The source is fairly well documented.  If you want further clarification on something, email or leave a comment.

Attached to this post is the Turbo C source code and executable.  The attachment is a zip file.  It was renamed to “.odt” to pass upload restrictions.  Rename it with a “.zip” extension and extract.  It has two files in it:

  • ABATCH.C
  • ABATCH.EXE

The 32 bit CRC checksum (cksum -o 3) of the file is (download link next to checksum):

3183384630 14242 ABatch-Zip.odt

 

Following is the source code listing.

/* ----------------------------------------------------------------------
// File.......: ABATCH.PRG
// Author.....: Ripdubski (inverseatascii@icloud.com)
// Date.......: 2019.10.10
// Description: 2019 re-creation of A-Batch written by myself in the
//              early 1990's whose source was lost.
//              A Batch script helper for displaying nice screens.
// Notes......:
// Revised....:
// ---------------------------------------------------------------------- */

/* ---------- Includes ---------- */
#include 
#include 
#include 
#include 
#include 
#include 


/* ---------- Definitions ---------- */
#define AB_VERSION "2.00"

/* Error codes */
#define AB_ERR_NONE       0
#define AB_ERR_BADSYNTAX  100
#define AB_ERR_BADCMD     101
#define AB_ERR_MISSING    200

/* Commands */
#define AB_CMD_BACK    "back"
#define AB_CMD_BOX     "box"
#define AB_CMD_CLS     "cls"
#define AB_CMD_GOXY    "goxy"
#define AB_CMD_HELP    "help"
#define AB_CMD_TEXT    "text"
#define AB_CMD_TITLE   "title"
#define AB_CMD_VERSION "version"
#define AB_CMD_WAIT    "wait"

/* Box border styles */
#define AB_STYLE_BACK   "back"
#define AB_STYLE_SINGLE "single"
#define AB_STYLE_DOUBLE "double"
#define AB_STYLE_BLOCK  "block"
#define AB_STYLE_FAT    "fat"
#define AB_STYLE_SD     "sd"
#define AB_STYLE_DS     "ds"
#define AB_STYLE_NONE   "none"

/* Color names */
#define AB_COLOR_BLACK    "black"
#define AB_COLOR_BLUE     "blue"
#define AB_COLOR_GREEN    "green"
#define AB_COLOR_CYAN     "cyan"
#define AB_COLOR_RED      "red"
#define AB_COLOR_MAGENTA  "magenta"
#define AB_COLOR_BROWN    "brown"
#define AB_COLOR_LGRAY    "lgray"
#define AB_COLOR_DGRAY    "dgray"
#define AB_COLOR_LBLUE    "lblue"
#define AB_COLOR_LGREEN   "lgreen"
#define AB_COLOR_LCYAN    "lcyan"
#define AB_COLOR_LRED     "lred"
#define AB_COLOR_LMAGENTA "lmagenta"
#define AB_COLOR_YELLOW   "yellow"
#define AB_COLOR_WHITE    "white"
#define AB_COLOR_BLINK    "blink"

/* Box character locations in the box character string */
#define AB_BOXCH_TL  0
#define AB_BOXCH_TR  2
#define AB_BOXCH_TM  1
#define AB_BOXCH_BL  6
#define AB_BOXCH_BR  4
#define AB_BOXCH_BM  5
#define AB_BOXCH_ML  3
#define AB_BOXCH_MR  7
#define AB_BOXCH_MM  8

/* Title ornament locations in the ornament character string */
#define AB_ORNCH_L 0
#define AB_ORNCH_R 1


/* ---------- Prototypes ---------- */
void idself(void);
int transcolor(char *);
void cmd_text(char *, int, int, char *, char *);
int main(int, char *[]);


/* ---------- Functions ---------- */

/* ----------------------------------------------------------------------
// Func.......: void idself(void)
// Description: Outputs program identifier
// Parameters.: n/a
// Returns....: n/a
// ---------------------------------------------------------------------- */
void idself(void)
{
    printf("A-Batch, version %s\nCopyright (c) 2019   Ripdubski   All Rights Reserved.\n", AB_VERSION);
}


/* ----------------------------------------------------------------------
// Func.......: int transcolor(char *cName)
// Description: Translates named color to color attribute value
// Parameters.: cName = color name
// Returns....: color attribute value (number)
// ---------------------------------------------------------------------- */
int transcolor(char *cName)
{
    int iColor = 0;

    /* Decide which color value to return */
    if (stricmp(cName, AB_COLOR_BLACK) == 0) iColor =    0;
    if (stricmp(cName, AB_COLOR_BLUE) == 0) iColor =     1;
    if (stricmp(cName, AB_COLOR_GREEN) == 0) iColor =    2;
    if (stricmp(cName, AB_COLOR_CYAN) == 0) iColor =     3;
    if (stricmp(cName, AB_COLOR_RED) == 0) iColor =      4;
    if (stricmp(cName, AB_COLOR_MAGENTA) == 0) iColor =  5;
    if (stricmp(cName, AB_COLOR_BROWN) == 0) iColor =    6;
    if (stricmp(cName, AB_COLOR_LGRAY) == 0) iColor =    7;
    if (stricmp(cName, AB_COLOR_DGRAY) == 0) iColor =    8;
    if (stricmp(cName, AB_COLOR_LBLUE) == 0) iColor =    9;
    if (stricmp(cName, AB_COLOR_LGREEN) == 0) iColor =   10;
    if (stricmp(cName, AB_COLOR_LCYAN) == 0) iColor =    11;
    if (stricmp(cName, AB_COLOR_LRED) == 0) iColor =     12;
    if (stricmp(cName, AB_COLOR_LMAGENTA) == 0) iColor = 13;
    if (stricmp(cName, AB_COLOR_YELLOW) == 0) iColor =   14;
    if (stricmp(cName, AB_COLOR_WHITE) == 0) iColor =    15;
    if (stricmp(cName, AB_COLOR_BLINK) == 0) iColor =    128;

    return(iColor);
}


/* ----------------------------------------------------------------------
// Func.......: void cmd_text(char *cStr, int iX, int iY, char *cFC, char *cBC)
// Description: Routine to output text and preserve text attributes
// Parameters.: cStr = string of text to display
//              iX = X coordinate to display at
//              iY = Y coordinate to display at
//              cFC = foreground color of text
//              cBC = background color of text
// Returns....: n/a
// ---------------------------------------------------------------------- */
void cmd_text(char *cStr, int iX, int iY, char *cFC, char *cBC)
{
    struct text_info sText;

    /* Save old mode */
    gettextinfo(&sText);

    /* Set text forground and background color */
    textcolor(transcolor(cFC));
    textbackground(transcolor(cBC));

    /* Output text at location */
    gotoxy(iX,iY);
    cputs(cStr);

    /* Restore text attributes */
    textattr(sText.attribute);
}


/* ----------------------------------------------------------------------
// Func.......: int main(int argc, char *argv[])
// Description: Main routine
// Parameters.: argc = number of command line arguments
//              argv = array of command line arguments
// Returns....: errorcode (can be tested/checked from BATCH script)
// ---------------------------------------------------------------------- */
int main(int argc, char *argv[])
{
    /* Default return value to badcmd, set as needed throughout */
    int iCnt, iLp, iRet = AB_ERR_BADCMD, iX = 0, iY = 0, iW = 0, iH = 0;
    /* Strings for box drawing */
    char cBoxBack[11] =   { 176,176,176,176,176,176,176,176,176,'\0' };
    char cBoxSingle[11] = { 218,196,191,179,217,196,192,179,32,'\0' };
    char cBoxDouble[11] = { 201,205,187,186,188,205,200,186,32,'\0' };
    char cBoxBlock[11] =  { 219,219,219,219,219,219,219,219,32,'\0' };
    char cBoxFat[11] =    { 220,220,220,222,223,223,223,221,32,'\0' };
    char cBoxSD[11] =     { 214,196,183,186,189,196,211,186,32,'\0' };
    char cBoxDS[11] =     { 213,205,184,179,190,205,212,179,32,'\0' };
    char cBoxNone[11] =   { 32,32,32,32,32,32,32,32,32,'\0' };
    char *cBox;  
    /* Strings for title ornaments */
    char cOrnSingle[4] = { 180,195,'\0' };
    char cOrnDouble[4] = { 181,198,'\0' };
    char cOrnBlock[4]  = { 221,222,'\0' };
    char cOrnFat[4]    = { 221,222,'\0' };
    char cOrnSD[4]     = { 180,195,'\0' };
    char cOrnDS[4]     = { 181,198,'\0' };
    char cOrnNone[4]   = { ' ',' ','\0' };
    char *cOrn;
    /* Temp buffers for building output */
    char cLine[81] = "", cMiddle[81] = "", cChL, cChM, cChR;

    /* Set actual argument count */
    iCnt = argc - 1;

    /* Check for proper syntax */
    if ((iCnt < 2) | (stricmp(argv[1], AB_CMD_HELP) == 0)) {
        idself();

	printf("Syntax: ascreen command parameters\n");
        printf("        commands: back style fc bc = draw background (style=back or 3 chars)\n");
        printf("                  box x y w h style fc bc = draw box in style at x y\n");
        printf("                  cls = clear screen and home cursor\n");
        printf("                  goxy x y = move cursor to x,y\n");
        printf("                  help = syntax\n");
        printf("                  text x y \"text\" fc bc = output \"text\" at x y\n");
        printf("                  title x y \"text\" fc bc style fc bc = box title\n");
        printf("                  wait n = wait n seconds\n");
        printf("                  version = version information\n");
        printf("        x y expressed in base 1\n");
        printf("        styles: single, double, block, fat, SD, DS, none, back (back only)\n");
        printf("        colors: black, blue, green, cyan, red, magenta, brown, lgray,\n");
        printf("                dgray, lblue, lgreen, lcyan, lred, lmagenta, yellow, white\n");

	exit(AB_ERR_BADSYNTAX);
    }

    /* Decide on command and take action */

    /* Output text in specific style at specific location */
    if (stricmp(argv[1], AB_CMD_TEXT) == 0) {
	if (iCnt < 6) {
	    iRet = AB_ERR_MISSING;
	} else {
	    /* text x y fc bc string */
            /* Grab x, y */
	    iX = atoi(argv[2]);
            iY = atoi(argv[3]);

            cmd_text(argv[6], iX, iY, argv[4], argv[5]);
            iRet = AB_ERR_NONE;
	}
    }
    /* Draw specific sized box in specific style at specific location */
    if (stricmp(argv[1], AB_CMD_BOX) == 0) {
	if (iCnt < 8) {
	    iRet = AB_ERR_MISSING;
	} else {
	    /* box x y w h style fc bc */
            /* Grab x, y, width, and height */
	    iX = atoi(argv[2]);
	    iY = atoi(argv[3]);
	    iW = atoi(argv[4]);
	    iH = atoi(argv[5]);

            /* Set build string based on selection type */
            if (stricmp(argv[6], AB_STYLE_BACK) == 0)   cBox = cBoxBack;
            if (stricmp(argv[6], AB_STYLE_SINGLE) == 0) cBox = cBoxSingle;
            if (stricmp(argv[6], AB_STYLE_DOUBLE) == 0) cBox = cBoxDouble;
            if (stricmp(argv[6], AB_STYLE_BLOCK) == 0)  cBox = cBoxBlock;
            if (stricmp(argv[6], AB_STYLE_FAT) == 0)    cBox = cBoxFat;
            if (stricmp(argv[6], AB_STYLE_SD) == 0)     cBox = cBoxSD;
            if (stricmp(argv[6], AB_STYLE_DS) == 0)     cBox = cBoxDS;
            if (stricmp(argv[6], AB_STYLE_NONE) == 0)   cBox = cBoxNone;

            /* Build box line by line and output */
            for (iLp = 1; iLp <= iH; iLp++) {
                /* Determine line characters based on line number */
                if (iLp == 1) {
                    cChL = cBox[AB_BOXCH_TL];
                    cChR = cBox[AB_BOXCH_TR];
                    cChM = cBox[AB_BOXCH_TM];
                } else {
                    if (iLp == iH) {
                        cChL = cBox[AB_BOXCH_BL];
                        cChR = cBox[AB_BOXCH_BR];
                        cChM = cBox[AB_BOXCH_BM];
                    } else {
                        cChL = cBox[AB_BOXCH_ML];
                        cChR = cBox[AB_BOXCH_MR];
                        cChM = cBox[AB_BOXCH_MM];
                    }
                }

                /* Create middle portion of line, then build entire line */
                memset(cMiddle, cChM, iW - 2);
                cMiddle[iW] = '\0';
                sprintf(cLine, "%c%s%c", cChL, cMiddle, cChR);

                /* Output the line */
                cmd_text(cLine, iX, iY + (iLp - 1), argv[7], argv[8]);
            }

            iRet = AB_ERR_NONE;
	}
    }
    /* Add title to box using blended delimiters */
    if (stricmp(argv[1], AB_CMD_TITLE) == 0) {
	if (iCnt < 9) {
	    iRet = AB_ERR_MISSING;
	} else {
	    /* title x y text fc bc style fc bc */
            /* Grab x, y, width, and height */
	    iX = atoi(argv[2]);
	    iY = atoi(argv[3]);

            /* Set build string based on selection type */
            if (stricmp(argv[7], AB_STYLE_SINGLE) == 0) cBox = cOrnSingle;
            if (stricmp(argv[7], AB_STYLE_DOUBLE) == 0) cBox = cOrnDouble;
            if (stricmp(argv[7], AB_STYLE_BLOCK) == 0)  cBox = cOrnBlock;
            if (stricmp(argv[7], AB_STYLE_FAT) == 0)    cBox = cOrnFat;
            if (stricmp(argv[7], AB_STYLE_SD) == 0)     cBox = cOrnSD;
            if (stricmp(argv[7], AB_STYLE_DS) == 0)     cBox = cOrnDS;
            if (stricmp(argv[7], AB_STYLE_NONE) == 0)   cBox = cOrnNone;

            /* Get ornaments */
            cChL = cBox[AB_ORNCH_L];
            cChR = cBox[AB_ORNCH_R];

            /* Create and output left and right ornaments */
            sprintf(cLine, "%c", cChL);
            cmd_text(cLine, iX, iY, argv[8], argv[9]);
            sprintf(cLine, "%c", cChR);
            cmd_text(cLine, iX + strlen(argv[4]) + 3, iY, argv[8], argv[9]);
            sprintf(cLine, " %s ", argv[4]);
            cmd_text(cLine, iX + 1, iY, argv[5], argv[6]);

            iRet = AB_ERR_NONE;
	}
    }
    /* Goto X Y (cursor) screen coordinates */
    if (stricmp(argv[1], AB_CMD_GOXY) == 0) {
	if (iCnt < 3) {
	    iRet = AB_ERR_MISSING;
	} else {
	    /* goxy x y */
	    iX = atoi(argv[2]);
	    iY = atoi(argv[3]);
	    gotoxy(iX, iY);

            iRet = AB_ERR_NONE;
	}
    }
    /* Wait number of seconds */
    if (stricmp(argv[1], AB_CMD_WAIT) == 0) {
	if (iCnt < 2) {
	    iRet = AB_ERR_MISSING;
	} else {
	    /* wait secs */
	    iX = atoi(argv[2]);
	    sleep(iX);

            iRet = AB_ERR_NONE;
        }
    }
    /* Draw background filling entire screen */
    if (stricmp(argv[1], AB_CMD_BACK) == 0) {
	if (iCnt < 4) {
	    iRet = AB_ERR_MISSING;
	} else {
	    /* back style fc bc */

            /* Set build string based on selection type - only back supported */
            if (stricmp(argv[2], AB_STYLE_BACK) == 0) {
                cBox = cBoxBack;
            } else {
                cBox = argv[2];   /* custom string (3 char minimum: LMR) */
            }

            /* Get background characters - only need first three */
            cChL = cBox[AB_BOXCH_TL];
            cChR = cBox[AB_BOXCH_TR];
            cChM = cBox[AB_BOXCH_TM];

            /* Create middle portion of line, then build entire line */
            memset(cMiddle, cChM, 78);
            cMiddle[79] = '\0';
            sprintf(cLine, "%c%s%c", cChL, cMiddle, cChR);

            /* Output line by line and output */
            for (iLp = 1; iLp <= 24; iLp++) {
                cmd_text(cLine, 1, iLp, argv[3], argv[4]);

                /* If line 24, write line, push down, return to 24 and write line -
                   prevent trailing cursor wrap. */
                if (iLp == 24) {
                    gotoxy(1, 24);
                    insline();
                    gotoxy(1, 24);
                    cmd_text(cLine, 1, iLp, argv[3], argv[4]);
                    gotoxy(1, 24);
                }
            }

            iRet = AB_ERR_NONE;
	}
    }
    /* Clear Le Screen :D - not really needed, implemented as first parser test */
    if (stricmp(argv[1], AB_CMD_CLS) == 0) {
	clrscr();

        iRet = AB_ERR_NONE;
    }
    /* Version information */
    if (stricmp(argv[1], AB_CMD_VERSION) == 0) {
	idself();

        iRet = AB_ERR_NONE;
   }

    /* Were done, exit with errorcode */
    return(iRet);
}

 

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Pinterest (Opens in new window)
  • Click to share on Pocket (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Twitter (Opens in new window)

Like this:

Like Loading...

Posts navigation

← Older Entries
Newer Entries →
  • Follow Unfinished Bitness on WordPress.com
  • Enter your email address to follow this blog and receive notifications of new posts by email.

    Join 40 other subscribers
  • Recent Posts

    • C Style! – Action! Library Conversion 10
    • C Style! – Action! Library Conversion 9
    • C Style! – Action! Library Conversion 8
    • C Style! – Action! Library Conversion 7
    • C Style! – Action! Library Conversion 6
  • Categories

    • Apple
    • Atari
    • Audio
    • BBS
    • Bookmarks
    • DD-WRT
    • General
    • Google
    • iOS
    • MSDOS
    • NAS
    • Notes
    • OSX
    • Programming
    • Router
    • Security
  • Tags

    1Password 8 bit 12v 110v 6502 Action! Alpine Assembly Atari 400 Atari 800 Atari XE Atari XL BASIC Bookmarks boombox BootScript C (CC65) Car Audio CDA7893 CIO CX430 DD-WRT Delicious DHCP DiskStation DNSMasq EverNote Fitbit fitness ghetto-blaster Google GTO605C HTTP Server iCloud instruction IOCB iOS iTunes JBL Local DNS Mac OSX Mac OS X Migrate online Opcode OSX Perl SimpleNote Synology Windows
  • Archives

    • September 2022
    • August 2022
    • July 2022
    • June 2022
    • December 2021
    • February 2021
    • December 2020
    • September 2020
    • October 2019
    • May 2018
    • April 2018
    • June 2017
    • April 2017
    • February 2017
    • December 2016
    • September 2016
    • August 2016
    • July 2016
    • June 2016
    • May 2016
    • April 2016
    • September 2015
    • August 2015
    • July 2015
    • May 2015
    • April 2015
    • March 2015
    • February 2015
    • January 2015
    • December 2014
    • November 2014
    • October 2014
    • September 2014
    • August 2014
    • July 2014
    • June 2014
    • May 2014
    • April 2014
    • March 2014
    • February 2014
    • December 2013
    • October 2013
    • June 2013
    • May 2013
    • March 2013
    • February 2013
    • January 2013
  • Meta

    • Register
    • Log in
    • Entries feed
    • Comments feed
    • WordPress.com
Blog at WordPress.com.
Unfinished Bitness
Blog at WordPress.com.
  • Follow Following
    • Unfinished Bitness
    • Join 40 other followers
    • Already have a WordPress.com account? Log in now.
    • Unfinished Bitness
    • Customize
    • Follow Following
    • Sign up
    • Log in
    • Report this content
    • View site in Reader
    • Manage subscriptions
    • Collapse this bar
 

Loading Comments...
 

    %d bloggers like this: