Raspberry Pi – Auto-start Kiosk Mode Browser

OK, this one is the simps. I have a Raspberry Pi connected to a monitor. Whenever the Pi starts up, I want it to automatically load my air quality dashboard in full-screen (kiosk) mode. I don’t want to use a keyboard, mouse or other user action, I just want it to start up and show a webpage, using the full-screen kiosk mode.

Here’s how to do it:

Edit the lxsession autostart file with the following:

sudo nano /etc/xdg/lxsession/LXDE-pi/autostart

At the bottom of the file, add:

@/usr/bin/chromium-browser --kiosk https://kis.support/h200

That’s it. Whenever your Pi reboots, it’ll load up the webpage (in this case https://kis.support/h200).

Pro Tip: The Rapsberry Pi’s default screensaver can be a pain in the neck. I disabled it by installing xscreensaver with:

sudo apt install xscreensaver

Then, click the Raspberry “Home” button > Preferences > Screensaver. In the top dropdown “Mode” menu, select “Disable Screen Saver”. Then sit back and get ready for some serious burn-in.

Campus Time Machine Backup Server Setup


Note: This is a draft post - published for Paul to find more easily. It's not complete as long as this paragraph is here, but the initial steps will be needed before progressing further.

Install Linux Mint

Setup VNC Access (per LinuxMint tutorial)

1. Remove the default Vino server:

sudo apt-get -y remove vino

2. Install x11vnc:

sudo apt-get -y install x11vnc

3. Create the directory for the password file:

sudo mkdir /etc/x11vnc

4. Create the encrypted password file:

sudo x11vnc –storepasswd /etc/x11vnc/vncpwd

You will be asked to enter and verify the password.  Then press Y to save the password file.

5. Create the systemd service file for the x11vnc service:

sudo nano /lib/systemd/system/x11vnc.service

Copy/Paste this code into the empty file:

[Unit]
Description=Start x11vnc at startup.
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/bin/x11vnc -auth guess -forever -noxdamage -repeat -rfbauth /etc/x11vnc/vncpwd -rfbport 5900 -shared

[Install]
WantedBy=multi-user.target

6. Reload Daemon

sudo systemctl daemon-reload

7. Enable the x11vnc service at boot time:

sudo systemctl enable x11vnc.service

8. Start the service:

Either reboot or

sudo systemctl start x11vnc.service

Enable ssh access

sudo apt install openssh-server

Nebula VPN Setup

For external VPN access, use node16 (Paul knows)

Static ip Address Setup

Info for setting static ip address and access from off campus

Samba Server Setup

Install Samba

sudo apt update
sudo apt install samba

Samba setup

mkdir /home/<user>/sambashare/
sudo nano /etc/samba/smb.conf

Add the following to smb.conf:

[sambashare]
    comment = Samba on Ubuntu
    path = /home/username/sambashare
    read only = no
    browsable = yes

Restart service and allow firewall traffic:

sudo service smb restart
sudo ufw allow samba

Set up User Accounts

sudo smbpasswd -a username

Building Student Name Index for Yearbooks

Edward, one of our 11th grade students on the school’s yearbook team wanted to build an index of page numbers where each student appeared. Traditionally, this would entail multiple students going through every page and recording the page number for each student appearing on that page. Inevitably, some students get missed, errors are made, and some pages get skipped.

Edward, recognizing an opportunity to leverage his programming skills, wrote a Python script to collect the information in a much more efficient manner. His script reads two columns from a Google Spreadsheet into lists named name and nameraw. Then, for each name in the lists, the script searches each page of the yearbook pdf and logs each page number where the name is located. Finally, this data is written back to the Google sheet.

This script leverages Google sheets, but with minor alteration, it could easily use locally stored files instead. Note that this script depends on a number of dependencies, so don’t forget to add those that you may not yet have installed (PyPDF2, re, pathlib, and the relevant Google auth and client libraries). If you’re unfamiliar with the Google python libraries, well, that will need to be another Google search. However, if you use local files rather than reading from a Google sheet, those libraries (and the code between the dashed lines) could be excluded. You would just need to replace those lines with a couple others that would open your csv file with the list(s) of names.

This is Edward’s script and I had no hand in creating or using it. If you want to use it yourself, you’re welcome to do so. If you get stuck, check with one of your programming students or teachers first. If you’re still stuck, comment below and I’ll help out if I can.

from __future__ import print_function
import os.path      
from PyPDF2 import PdfFileReader, PdfFileWriter
from pathlib import Path
import re
import PyPDF2

#-----------------------------------------------------
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from google.oauth2 import service_account

# Note that you will need to set up your own Google API credentials if
# you're using Google Sheets. This part is beyond the scope of this post.
# But you can find how to do this with a quick google search. 
SERVICE_ACCOUNT_FILE = 'keys.json'
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
creds = None
creds = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE, scopes=SCOPES)

# The ID and range of the spreadsheet. Note that the ID below is fake
# (we're not going to post the real one on the interwebs)
# Make sure you update the ID to refer to your spreadsheet
SPREADSHEET_ID = '1DQyjh1-7T9jjU7kmg6-TNcjsCtnCspWiMbv3t2mJeM'

service = build('sheets', 'v4', credentials=creds)

# Call the Sheets API
sheet = service.spreadsheets()
result = sheet.values().get(spreadsheetId=SPREADSHEET_ID,
                            range="Sheet1!B1:B508").execute()
result2 = sheet.values().get(spreadsheetId=SPREADSHEET_ID,
                            range="Sheet1!A1:A508").execute()
#------------------------------------------------------------
values = result.get('values')
values2 = result2.get('values')

name = []
nameraw = []
#put names from values and values2 into lists
for x in values:
        for i in x:
                name.append(i)

for x in values2:
        for i in x:
                nameraw.append(i)

#Open the Yearbook pdf file
pdf = PdfFileReader('yearbook1.pdf')

#create a 2-D array for names & associated page numbers
pagesfinal = [[]]

#This is where everything happens. For each name, add it to pagesfinal.
#Then open each page in the pdf file and search it for the current name.
#If a version of the name is found, append the page number to the student's page list.

for i in range(len(name)):
        print(nameraw[i])
        pagesfinal.append([])
        for page_num in range (pdf.numPages):    
                pageobj = pdf.getPage(page_num)
                pageinfo = pageobj.extractText()
                pageinfo = ''.join(pageinfo.split())
                name[i]= ''.join(str(name[i]).split())
                nameraw[i] = ''.join(str(nameraw[i]).split())
                if (re.search(name[i], pageinfo)) or (re.search(nameraw[i], pageinfo)) or (re.search(name[i].upper(), pageinfo)):
                        pagesfinal[i].append(page_num +1)
                        print(pagesfinal[i])

#When the above loop completes, the following line writes the data back to the Google sheet
        request = sheet.values().update(spreadsheetId=SPREADSHEET_ID, range="Sheet1!C1", valueInputOption = "USER_ENTERED", body={"values":pagesfinal}).execute()

Receive Raspberry Pi’s ip address on boot

I do a lot of Raspberry Pi projects – often creating them in one place and using them in another. While it is easy enough to use SSH, VNC or HTML to access a device, if it’s being moved from one network to another, it can be challenging to know its ip address. Sure, you may be able to log into a local router and look it up, but I thought it would be easier if I just had the RPi email me with its current ip address.

Thankfully, Cody Giles created a tutorial on elinux.com outlining how to do this. His Python script runs on the RPi on startup, finds its ip address(es) and then emails this information to a predetermined address. I started with his code, but ended up changing it to meet my needs.

In order to allow a Python script to send email through a Gmail account, you may need to enable an “App Password” for your sending account.

Step #1: Create the Python Script

Copy the following and paste it into a text editor. You must change the ‘to’ address, as well as the ‘gmail_user’ and ‘gmail_password’ for the sending account in the lines below the pound sign lines about 25 lines from the top. Save this file as a python script in the home directory named startup_email.py.

import subprocess
import smtplib
from email.mime.text import MIMEText
import datetime
import os

# Account Information
##################################################################
to = 'recipientemail@server.com' # Email to send to.
gmail_user = 'senderaddress@gmail.com' # Email to send from. (MUST BE GMAIL)
gmail_password = 'emnopqoodnzcvkir' # Gmail app password.
smtpserver = smtplib.SMTP('smtp.gmail.com', 587) # Server to use.

smtpserver.ehlo()  # Says 'hello' to the server
smtpserver.starttls()  # Start TLS encryption
smtpserver.ehlo()
smtpserver.login(gmail_user, gmail_password)  # Log in to server
today = datetime.date.today()  # Get current time/date

arg='ip route list'  # Linux command to retrieve ip addresses.
# Runs 'arg' in a 'hidden terminal'.

p=os.popen("ifconfig | grep 'inet '").read()
#data = p.communicate()  # Get data from 'p terminal'.

print("p=")
print(p)
print("END p")

iplist = os.system("ifconfig | grep 'inet '");
print("iplist = "+str(iplist))
print("******************")
print(iplist)

# Creates the text, subject, 'from', and 'to' of the message.
msg = MIMEText("IP Addresses:" + "\n" + str(p))
msg['Subject'] = 'IPs For RaspberryPi on %s' % today.strftime('%b %d %Y')
msg['From'] = gmail_user
msg['To'] = to
# Sends the message
smtpserver.sendmail(gmail_user, [to], msg.as_string())
# Closes the smtp server.
smtpserver.quit()

Step #2: Make the script executable.

Using Terminal, navigate to the home directory and make the script executable.

cd ~
sudo chmod +x startup_email.py

Setp #3: Run the Script on Startup.

We’ll add a line to boot.rc to run the script whenever the RPi boots.

sudo nano /boot/boot.rc

Add the following lines to boot.rc

python /home/pi/Code/startup_email.py

Now, reboot the Pi and in about a minute, you should receive an email with your device’s IP address.

Forcing DNS on macOS in Recovery Mode

When resetting a 2020 MacBook Air, we ran into an issue where the device just wouldn’t connect to Apple’s servers. It felt like a DNS issue, but I couldn’t find a way to see or set DNS values. After some searching, I came across this Stack Overflow post with the solution. As that site was sold to a tech investment firm in June, 2021 (promising not to change a thing), I thought it best to preserve it here.


Here it is:

Usually DNS-server setup in the shell is done with sudo networksetup ... – a tool not available in the Base OS X System of the Recovery Mode.

You should still be able to change the DNS server with scutil in Terminal.app:

  1. Open Terminal.app in the menubar > Utilities
  2. Enter scutil --dns to get your current DNS config
  3. Enter scutil to reach interactive mode
  4. Enter list to get a list of all keys in the data store
  5. If you have several interfaces (you’ve found several State:/Network/Service/SERVICE_ID/IPv4 entries) determine the one connected to the Internet (based on e.g. your router and its internal network IP settings) – example:get State:/Network/Service/EB40E2FC-8248-48F2-8567-257D940A31EB/IPv4 d.show Example output:<dictionary> { Addresses : <array> { 0 : 192.168.0.8 } ConfigMethod : Manual SubnetMasks : <array> { 0 : 255.255.255.0 } } If your router has the IP-address 192.168.0.1 this should be the proper interface. If your router has e.g. the IP address 192.168.1.1 the interface found above would be the wrong one and you have to search for an interface with an IP in the range 192.168.1.2-192.168.1.254.
  6. Enter get State:/Network/Service/EB40E2FC-8248-48F2-8567-257D940A31EB/DNS use the service ID of the interface connected to the Internet you have found previously (here EB40E2FC-8248-48F2-8567-257D940A31EB)Entering d.show should show something like:<dictionary> { SearchDomains : <array> { 0 : some.domain } ServerAddresses : <array> { 0 : 192.168.0.1 } } Depending on the DHCP setup of your router the SearchDomains entry and array may be missing.
  7. Enter d.add ServerAddresses * 8.8.8.8 9.9.9.9 – add one or more DNS-server (here Google’s 8.8.8.8 and quad9’s 9.9.9.9)
  8. Enter set State:/Network/Service/EB40E2FC-8248-48F2-8567-257D940A31EB/DNS
  9. Enter d.show to check the modified dict entry. It should show something like:<dictionary> { SearchDomains : <array> { 0 : some.domain } ServerAddresses : <array> { 0 : 8.8.8.8 1 : 9.9.9.9 } }
  10. Enter quit to leave the interactive mode of scutil and return to the shell.
  11. Enter scutil --dns or dig to verify your new DNS config.

Remove Jamf framework

To remove JAMF’s Self Service App and all jamf components, enter the following in a Terminal window:

sudo /usr/local/bin/jamf removeFramework

Keep in mind that this will also remove things like any WiFi passwords that were installed using Jamf policies.

IF THE TERMINAL APP IS NOT ACCESSIBLE:

An old version of our MDM setup disabled student access to the Terminal app. Getting it back takes some doing:

  1. Create a new Admin user account.
  2. Log into the new Admin account.
  3. In Applications > Utilities, copy the Terminal app and paste a copy on the Desktop.
  4. Rename the Desktop Terminal app to T2.
  5. Run the T2 app.
  6. paste “sudo /usr/local/bin/jamf removeFramework” in the T2 app and press return.
    (This should remove the JamfFramework, MDM settings and Self Service app.)
  7. If successful, log out and log back into the student’s account and delete the just-created admin account.

Use INDEX with MATCH instead of VLOOKUP in Spreadsheets

VLOOKUP returns janky values if the value you’re looking up doesn’t exist in the lookup column. Combine INDEX and MATCH for more reliable results.

For the long explanation, head over to Ablebits. If you’re Paul and just looking for a reminder of the format, here it is:
=INDEX (column to return a value from, MATCH (lookup valuecolumn to lookup against, 0))

NWEA MAP Import and Export from PowerSchool

This post was shamelessly stolen from PSUG-Asia and is posted here only in case the referenced post disappears. [note to Paul: search your Google Drive for “NWEA MAP Rosters Directions” for a helpful spreadsheet]

This content is from Douglas Sirkka, Educational Technology Coordinator and PowerSchool Administrator from St. Mary’s International School.

You can download the PowerSchool Export Template File Here: MAPExportMap

Or Copy the code below and make your own:

TITLE CONTENTS

School Name ^(decode;^([schools]name);<<your school names here>>
Instructor Last Name [teachers]last_name
Instructor Middle Initial
Bus_Stop
Student First Name [students]first_name
Student Gender [students]gender
Previous Instructor ID
Bus_Stop
Period (Delete)
Expression Instructor ID [teachers]teachernumber
Instructor First Name [teachers]first_name
User Name [teachers]Email_addr
Email Address [teachers]Email_addr
Class Name [courses]course_name
Student ID [students]student_number
Student Date Of Birth [students]DOB
Student Grade [students]grade_level
Student Ethnic
Group Name
Bus_Stop
Student Last Name [students]last_name
Previous Student ID
Bus_Stop
Student Middle Initial
Bus_Stop
*Where it says ‘Bus_Stop’ that’s where I needed a blank in the file. With the TSV file above you can sort by SEQ field.

MAP Testing CRF Import In PowerSchool

  1. select the students who you need to create/update on NWEA.
    1. This will usually be the entire ES, the entire MS, or grades 9 and 10.
  2. In Group Functions, select Export Using a Template Type of Export: Student Schedules
  3. Export template: MapsCourseInforExport
    1. Select Only schedules for the selected… and Submit.
  4. Save the resulting export file and then open.
  5. Filter and select courses you want to associate with student profiles.
  6. Delete Period/Expression column at the end of the file. (this is for sorting/filtering purposes)
  7. Save as CSV (required)
  8. Login to NWEA Select Import Profiles
  9. Select Start New Import Select Combined
  10. Select the appropriate term.
  11. Add the Roster File.
  12. Click Next Check the data (Check student DOB this sometimes gets changed to a number) Reconcile Errors (Beyond scope of theses instructions but pretty straight forward)
  13. I think there is an error in the Export Template the needs a capital S in Not Specified… RP (Grade_Level 0) needs to be reconciled with K.
  14. Post the valid records.

Hope this makes your job a little easier.

Get your Global Entry Approval (and TSA Pre✓ authorization) Fast

Global Entry LogoA few years ago I signed up for the US Custom and Border Patrol (CBP) Global Entry Program available to Americans and others. Getting approved and acquiring a “Known Traveler Number” (KTN) allows me to skip the lines at passport control in the US, using Global Entry kiosks instead. Adding my KTN number to my airline accounts also results in the “TSA Pre” stamp automagically appearing on my boarding passes. This lets me take the faster, shorter lanes at TSA checkpoints, leaving my laptop in its case, shoes on my feet and belt on my jeans.

I arranged an appointment at O’Hare airport for about 6 weeks in the future, the earliest available appointment at the time. I burned frequent flyer miles to fly down from my local airport just for the appointment. Unfortunately, when I got to the CBP office at O’Hare, I realized I had flown down one day too early. The CBP agent at the counter had me stick around and about an hour later they fit me in between appointments. I had a short interview, they took my picture and sent me on my way. My card and KTN arrived a few weeks later in the mail.

I was reminded of this experience today when I ran across a Lifehacker article suggesting that you might try just walking in when you have an extended layover in an airport with a CBP Enrollment Center. You’ll want to make sure you’ve gone through the steps outlined in the “How to Apply” page first. But then try walking in and getting in without an appointment.

Cheap Passport Photos

Sooner or later, you’re going to need passport photos. Get them now and have them when you need them. I do this by filling an 8×10 print with passport photos and sending it to the local Walgreens photo department. This gives me 20 2×2 passport photos for $3.99 rather than 2 passport photos for $13.99 using Walgreens passport photo service. If you need information for visa photo requirements for a specific country, check visafoto.com.

The following instructions are for US passport photos (2″ x 2″) but can be adapted for other photo sizes. First, get a headshot photo that satisfies the necessary requirements. Then get the photo into PhotoShop or your favorite photo editing program and crop it to a square to look something like this:

Dwight D Eisenhower Portrait
Dwight D Eisenhower

Next, resize the image to 2 inches square and set the canvas size to 8 inches by 10 inches. Duplicate the 2×2″headshot three times and arrange the image four layers across the top of the canvas. You should have one row complete, looking something like this:

Then merge those four image layers into one and duplicate it four more times, arranging them to fill the 8×10:

Now just flatten the image, save it as a jpg file and upload it to your Walgreens Photo account. Print it as an 8×10 for $3.99 and pick it up in an hour or just have it mailed to you for $0.99 more.

Pro tip: Check the Walgreens Photo Promo page, look for the current month’s special discount code. There’s often a 40% off or free 8×10 code you can enter at checkout for an even better deal.