Home Assistant Setup on Kubernetes

Install the k8s cluster if not alreadly installed

1
2
microk8s enable dns storage helm3
microk8s status

Add the helm Repo

1
microk8s helm repo add alekc-charts https://charts.alekc.dev/

Install the helm chart

1
microk8s helm install home-assistant alekc-charts/home-assistant

Check the status of the installed application.

1
2
microk8s status
microk8s kubectl describe pods

Service yaml file

We need to expose the service to the outside world.
Thankfully microk8s has a built in loadbalancer called metallb

1
nano home-assistant-service.yaml

Replace port_number with the real port number

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
name: home-assistant-service
spec:
type: LoadBalancer
selector:
app.kubernetes.io/name: home-assistant
ports:
- name: webportal
protocol: TCP
port: port_number
targetPort: port_number
externalIPs:
- A.B.C.D

Apply the service

1
microk8s kubectl apply -f ./home-assistant-service.yaml

Confirm the service is active

1
microk8s kubectl describe services home-assistant-service

Firewall Rules

This assumes you are using ufw.
ufw is bascally a wrapper for IPTABLES. If you have ever used IPTABLES before you understand why ufw exists.
Replace port_number with the real port number

1
2
3
4
sudo ufw default allow routed 
sudo ufw allow from A.B.C.0/24 to any port port_number proto tcp
sudo ufw allow from D.E.F.0/24 to any port port_number proto tcp
sudo ufw status

Jellyfin Media Server Setup on Kubernetes

Install the k8s cluster if not alreadly installed

1
2
microk8s enable dns storage helm3
microk8s status

Add the Truecharts Repo

1
microk8s helm repo add truecharts https://charts.truecharts.org/

Pull the helm chart

1
microk8s helm pull truecharts/jellyfin 

To enable DLNA we need to enable access to the host network.

Host Access addon

1
microk8s enable host-access:ip=A.B.C.D

Modify the values.yaml file to set the configuration

1
2
3
tar -xvf ./jellyfin-*.tgz
cd jellyfin
cat values.yamls

Make the data directory

APPLICATION_ROOT_DIRECTORY: Path where the application config and data files are stored

1
2
3
4
mkdir -p APPLICATION_ROOT_DIRECTORY/config
mkdir -p APPLICATION_ROOT_DIRECTORY/data
sudo chown -R 777 APPLICATION_ROOT_DIRECTORY

Create Persistent Volumes and Persistent Volume Claims

Persistent Volumes

Config

jellyfin-config-pv.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: jellyfin-config-pv
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: APPLICATION_ROOT_DIRECTORY/config # This must exist on the host
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- dionysus
1
microk8s kubectl apply -f jellyfin-config-pv.yaml

Data

jellyfin-data-pv.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: jellyfin-data-pv
spec:
capacity:
storage: 5Ti
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: APPLICATION_ROOT_DIRECTORY/data # This must exist on the host
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- dionysus
1
microk8s kubectl apply -f jellyfin-data-pv.yaml
1
microk8s kubectl get pv 
1
2
3
NAME                               CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                       STORAGECLASS    REASON   AGE
jellyfin-config-pv 100Gi RWO Retain Bound default/jellyfin-config-pvc local-storage 98s
jellyfin-data-pv 5Ti RWO Retain Bound default/jellyfin-data-pvc local-storage 78s

Persistent Volume Claims

Config

jellyfin-config-pvc.yaml
Bonds to jellyfin-config-pv

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-config-pvc
spec:
storageClassName: local-storage # Empty string must be explicitly set otherwise default StorageClass will be set
accessModes:
- ReadWriteOnce
volumeName: jellyfin-config-pv
resources:
requests:
storage: 100Gi
1
microk8s kubectl apply -f jellyfin-config-pvc.yaml

Data

jellyfin-data-pvc.yaml
Bonds to jellyfin-data-pv

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-data-pvc
spec:
storageClassName: local-storage # Empty string must be explicitly set otherwise default StorageClass will be set
accessModes:
- ReadWriteOnce
volumeName: jellyfin-data-pv
resources:
requests:
storage: 5Ti
1
microk8s kubectl apply -f jellyfin-data-pvc.yaml
1
microk8s kubectl get pvc
1
2
3
NAME                                STATUS   VOLUME                             CAPACITY   ACCESS MODES   STORAGECLASS    AGE
jellyfin-config-pvc Bound jellyfin-config-pv 100Gi RWO local-storage 26s
jellyfin-data-pvc Bound jellyfin-data-pv 5Ti RWO local-storage 12s

Modify the values.yaml file to set the configuration

We’re going to set the jellyfin configuration within the container.

1
2
cd jellyfin
nano values.yaml

DLNA_PORT: Port the dlna server will listen on
AUTO_DISCOVER_PORT: DLNA autodiscover port
WEB_GUI_PORT: Port to access the web gui

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
image:
repository: tccr.io/truecharts/jellyfin
pullPolicy: IfNotPresent
tag: v10.8.10@sha256:d2c377ee7ea463110a1dda7eb1b231424ad05245aaf95744e76164bd2e593377
broadcastProxyImage:
repository: tccr.io/truecharts/socat
pullPolicy: IfNotPresent
tag: v1.7.4.4@sha256:2417f121a5fd08012c927bfb7cdedb00ce97e0ec53fbf38352be8c6f197f7294
service:
main:
ports:
main:
port: WEB_GUI_PORT
targetPort: WEB_GUI_PORT
autodiscovery:
enabled: true
ports:
autodiscovery:
enabled: true
protocol: udp
port: AUTO_DISCOVER_PORT
hostPort: AUTO_DISCOVER_PORT
hostIP: A.B.C.D
dlna:
enabled: true
ports:
dlna:
enabled: true
protocol: udp
port: DLNA_PORT
hostPort: DLNA_PORT
hosstIP: A.B.C.D

persistence:
config:
enabled: true
mountPath: "/config"
type: pvc
existingClaim: jellyfin-config-pvc
cache:
enabled: true
mountPath: "/cache"
type: "emptyDir"
transcode:
enabled: true
mountPath: "/config/transcodes"
type: "emptyDir"
media:
enabled: true
mountPath: /media
type: pvc
existingClaim: jellyfin-data-pvc
portal:
open:
enabled: true
securityContext:
container:
readOnlyRootFilesystem: false
workload:
main:
podSpec:
hostNetwork: true
containers:
main:
env:
JELLYFIN_PublishedServerUrl: "{{ $.Values.chartContext.APPURL }}"
broadcastproxy:
enabled: false
type: DaemonSet
podSpec:
hostNetwork: true
# Proxy doesn't seem to respect the TERM signal, so by default
# this ends up just hanging until the default grace period ends.
# This is unnecesary since this workload only proxies autodiscovery
# messages.
terminationGracePeriodSeconds: 3
containers:
broadcastproxy:
enabled: true
primary: true
imageSelector: broadcastProxyImage
securityContext:
readOnlyRootFilesystem: true
command: ["/bin/sh"]
# Quite a lot going on here:
# - Resolve Jellyfin's autodiscovery service IP from its FQDN via getent hosts
# - Export the IP to `$TARGET_IP`
# - Check `$TARGET_IP` is not empty (so we can crash if it is - will help to detect templating errors)
# - Touch `/tmp/healty` to use with the readiness, liveness and startup probes
# - Start socat in proxy mode
# - On exit remove `/tmp/healthy`
args: ["-c", "export TARGET_IP=$(getent hosts '{{ printf \"%v-autodiscovery\" (include \"tc.v1.common.lib.chart.names.fullname\" $) }}' | awk '{ print $1 }') && [[ ! -z $TARGET_IP ]] && touch /tmp/healthy && socat UDP-LISTEN:AUTO_DISCOVER_PORT,fork,reu>
probes:
readiness:
enabled: true
type: exec
command:
- cat
- /tmp/healthy
liveness:
enabled: true
type: exec
command:
- cat
- /tmp/healthy
startup:
enabled: true
type: exec
command:
- cat
- /tmp/healthy

# -- enable Jellyfin autodiscovery on LAN
autodiscovery:
enabled: true

Install the helm chart with our values.yaml file

1
microk8s helm install jellyfin truecharts/jellyfin --values ./values.yaml 

Check the status of the installed application.

1
2
microk8s status
microk8s kubectl describe pods

Common Errors

Error from server (BadRequest): container in pod is waiting to start: ContainerCreating
You probably need to change the permissions on the PV directory. This path is what is written in the PersistentVolume in the path variable. A quick chmod -R 777 to this path will most likely fix the issue. The conatiner should update the permissions once it runs.

Service yaml file

Since we are using the hostnetwork we don’t need the an additional service file to route information.

More information here
https://medium.com/swlh/kubernetes-external-ip-service-type-5e5e9ad62fcd

Firewall Rules

This assumes you are using ufw.
ufw is bascally a wrapper for IPTABLES. If you have ever used IPTABLES before you understand why ufw exists.
https://docs.syncthing.net/users/firewall.html

1
sudo ufw default allow routed 
1
2
3
sudo ufw allow from A.B.C.0/24 to any port WEB_GUI_PORT proto tcp
sudo ufw allow from A.B.C.0/24 to any port AUTO_DISCOVER_PORT proto udp
sudo ufw allow from A.B.C.0/24 to any port DLNA_PORT proto udp
1
sudo ufw status

References

https://kubernetes.io/docs/concepts/services-networking/service/

Zigbee2Mqtt Kubernetes Setup on Ubuntu

Install the k8s cluster if not alreadly installed

1
2
microk8s enable dns storage helm3
microk8s status

Pull the helm chart

1
2
microk8s helm repo add truecharts https://charts.truecharts.org/
microk8s helm pull truecharts/zigbee2mqtt --version 7.0.30

Modify the values.yaml file to set the configuration

1
2
3
tar -xvf ./zigbee2mqtt-*.tgz
cd zigbee2mqtt
cat values.yamls
1
2
3
4
5
6
EXTERNAL_IP: A.B.C.D
EXTERNAL_NETWORK: A.B.C.0
SERVICE_PORT: Port used by the application
ZIGBEE_ADAPTER_IP: Ip address of the zigbee adapter
ZIGBEE_ZIGBEE_ADAPTER_PORT: Port used by the external zigbee adapter
MQTT_PORT: PORT_C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
image:
repository: tccr.io/truecharts/zigbee2mqtt
tag: v1.32.1@sha256:f3bcf64a1a538ce5636b7359bd4f0375f593c9408d561a1add9f3d2bed843bf3
pullPolicy: IfNotPresent
service:
main:
ports:
main:
port: SERVICE_PORT
persistence:
data:
enabled: true
mountPath: "/data"
targetSelectAll: true

portal:
open:
enabled: true
securityContext:
container:
runAsNonRoot: false
readOnlyRootFilesystem: false
runAsUser: 0
runAsGroup: 0
workload:
main:
podSpec:
initContainers:
init-config:
enabled: true
imageSelector: image
type: init
env:
ZIGBEE2MQTT_CONFIG_FRONTEND_PORT: "{{ .Values.service.main.ports.main.port }}"
ZIGBEE2MQTT_CONFIG_EXPIRIMENTAL_NEW_API: "{{ .Values.workload.main.podSpec.containers.main.env.ZIGBEE2MQTT_CONFIG_EXPIRIMENTAL_NEW_API }}"
ZIGBEE2MQTT_CONFIG_PERMIT_JOIN: "{{ .Values.workload.main.podSpec.containers.main.env.ZIGBEE2MQTT_CONFIG_PERMIT_JOIN }}"
ZIGBEE2MQTT_CONFIG_MQTT_SERVER: "{{ .Values.workload.main.podSpec.containers.main.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER }}"
ZIGBEE2MQTT_CONFIG_MQTT_USER: "{{ .Values.secret.ZIGBEE2MQTT_CONFIG_MQTT_USER }}"
ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD: "{{ .Values.secret.ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD }}"
ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC: "{{ .Values.workload.main.podSpec.containers.main.env.ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC }}"
ZIGBEE2MQTT_CONFIG_SERIAL_PORT: "{{ .Values.workload.main.podSpec.containers.main.env.ZIGBEE2MQTT_CONFIG_SERIAL_PORT }}"
ZIGBEE2MQTT_CONFIG_SERIAL_ADAPTER: "{{ .Values.workload.main.podSpec.containers.main.env.ZIGBEE2MQTT_CONFIG_SERIAL_ADAPTER }}"
USE_CUSTOM_CONFIG_FILE: "{{ .Values.workload.main.podSpec.containers.main.env.USE_CUSTOM_CONFIG_FILE }}"
command:
- /bin/sh
- -c
args:
- >
if [ -f /data/configuration.yaml ] || [ ${USE_CUSTOM_CONFIG_FILE} == true ]; then

echo "Initial configuration exists or User selected to use custom configuration file. Skipping...";
else

echo "Creating initial configuration";
touch /data/configuration.yaml;
echo "# Configuration bellow will be always be overridden" >> /data/configuration.yaml;
echo "# from environment settings on the Scale Apps UI." >> /data/configuration.yaml;
echo "# You however will not see this values change in the file." >> /data/configuration.yaml;
echo "# It's a generated file based on the values provided on initial install." >> /data/configuration.yaml;
echo "##########################################################" >> /data/configuration.yaml;
echo "experimental:" >> /data/configuration.yaml;
echo " new_api: $ZIGBEE2MQTT_CONFIG_EXPIRIMENTAL_NEW_API" >> /data/configuration.yaml;
echo "frontend:" >> /data/configuration.yaml;
echo " port: $ZIGBEE2MQTT_CONFIG_FRONTEND_PORT" >> /data/configuration.yaml;
echo "permit_join: $ZIGBEE2MQTT_CONFIG_PERMIT_JOIN" >> /data/configuration.yaml;
echo "mqtt:" >> /data/configuration.yaml;
echo " server: $ZIGBEE2MQTT_CONFIG_MQTT_SERVER" >> /data/configuration.yaml;
echo " base_topic: $ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC" >> /data/configuration.yaml;
if [ ! -z "$ZIGBEE2MQTT_CONFIG_MQTT_USER" ];
then
echo " user: $ZIGBEE2MQTT_CONFIG_MQTT_USER" >> /data/configuration.yaml;
fi;
if [ ! -z "$ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD" ];
then
echo " password: $ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD" >> /data/configuration.yaml;
fi;
echo "serial:" >> /data/configuration.yaml;
echo " port: $ZIGBEE2MQTT_CONFIG_SERIAL_PORT" >> /data/configuration.yaml;
echo " adapter: $ZIGBEE2MQTT_CONFIG_SERIAL_ADAPTER" >> /data/configuration.yaml;
echo "##########################################################" >> /data/configuration.yaml;
echo 'Initial configuration file created at "/data/configuration.yaml"';
fi;
containers:
main:
env:
ZIGBEE2MQTT_DATA: "/data"
ZIGBEE2MQTT_CONFIG_FRONTEND_PORT: "{{ .Values.service.main.ports.main.port }}"
# User defined
USE_CUSTOM_CONFIG_FILE: false
# This values are required for the autogenerated file to work.
ZIGBEE2MQTT_CONFIG_EXPIRIMENTAL_NEW_API: false
ZIGBEE2MQTT_CONFIG_PERMIT_JOIN: false
ZIGBEE2MQTT_CONFIG_MQTT_SERVER: "mqtt://mosquitto:MQTT_PORT"
ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC: "zigbee2mqtt"
ZIGBEE2MQTT_CONFIG_SERIAL_PORT: "tcp://ZIGBEE_ADAPTER_IP:ZIGBEE_ADAPTER_PORT"
ZIGBEE2MQTT_CONFIG_SERIAL_ADAPTER: "auto"

ZIGBEE2MQTT_CONFIG_MQTT_USER: ""
ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD: ""

Service

zigbee2mqtt-service.yaml

SERVICE_PORT: Port used to access the application
ZIGBEE_ADAPTER_PORT: Port used to allow access to an external zigbee controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Service
metadata:
name: zigbee2mqtt-service
spec:
type: LoadBalancer
selector:
app.kubernetes.io/name: zigbee2mqtt
ports:
- name: http
protocol: TCP
port: SERVICE_PORT
targetPort: SERVICE_PORT
- name: adapter
protocol: TCP
port: ZIGBEE_ADAPTER_PORT
targetPort: ZIGBEE_ADAPTER_PORT
externalIPs:
- A.B.C.D

Firewall

sudo ufw allow from A.B.C.0/24 to any port ZIGBEE_ADAPTER_PORT proto tcp
sudo ufw allow from A.B.C.0/24 to any port SERVICE_PORT proto tcp

Configuration

https://www.reddit.com/r/homeassistant/comments/zxee4n/zigbee2mqtt_error_failed_to_connect_to_the/

Transmission Kubernetes Setup on Ubuntu

Install the k8s cluster if not alreadly installed

1
2
microk8s enable dns storage helm3
microk8s status

Pull the helm chart

1
2
microk8s helm repo add truecharts https://charts.truecharts.org/
microk8s helm pull truecharts/transmission

Modify the values.yaml file to set the configuration

1
2
3
tar -xvf ./transmission-*.tgz
cd transmission
cat values.yamls

Make the data directory

CONTAINER_ROOT_PATH

1
2
3
mkdir -p CONTAINER_ROOT_PATH
sudo chown -R 777 CONTAINER_ROOT_PATH

Create Persistent Volumes and Persistent Volume Claims

Transmission can think you deleted all your files if the harddisk containing your files fails to mount and the configuration files are on a different disk.

Persistent Volumes

Config

transmission-config-pv.yaml

HOSTNAME: Local computer hostname

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: transmission-config-pv
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: CONTAINER_ROOT_PATH/config # This must exist on the host
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- HOSTNAME

microk8s kubectl apply -f transmission-config-pv

1
2
3
microk8s kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
transmission-config-pv 100Gi RWO Retain Bound default/transmission-config-pvc local-storage 9m44s

Data

transmission-data-pv.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: transmission-data-pv
spec:
capacity:
storage: 5Ti
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: CONTAINER_ROOT_PATH/data # This must exist on the host
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- HOSTNAME

microk8s kubectl apply -f transmission-data-pv.yaml

microk8s kubectl get pv

1
2
3
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                 STORAGECLASS        REASON   AGE
transmission-config-pv 100Gi RWO Retain Bound default/transmission-config-pvc local-storage 3h28m
transmission-data-pv 5Ti RWO Retain Bound default/transmission-data-pvc local-storage 19s

Persistent Volume Claims

Config

transmission-config-pvc.yaml
Bonds to transmission-config-pv

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: transmission-config-pvc
spec:
storageClassName: local-storage # Empty string must be explicitly set otherwise default StorageClass will be set
accessModes:
- ReadWriteOnce
volumeName: transmission-config-pv
resources:
requests:
storage: 100Gi

microk8s kubectl apply -f transmission-config-pvc.yaml

Data

transmission-data-pvc.yaml
Bonds to transmission-data-pvc

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: transmission-data-pvc
spec:
storageClassName: local-storage # Empty string must be explicitly set otherwise default StorageClass will be set
accessModes:
- ReadWriteOnce
volumeName: transmission-data-pv
resources:
requests:
storage: 5Ti

microk8s kubectl apply -f transmission-data-pvc.yaml

microk8s kubectl get pvc

1
2
3
NAME                          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS        AGE
transmission-config-pvc Bound transmission-config-pv 100Gi RWO local-storage 3h21m
transmission-data-pvc Bound transmission-data-pv 5Ti RWO local-storage 51s

Pull the syncthing helm chart

1
2
3
microk8s helm repo add truecharts https://charts.truecharts.org/
microk8s helm repo update
microk8s helm pull truecharts/transmission

Modify the values.yaml file to set the configuration

We’re going to set the transmission configuration within the container.

1
2
3
tar -xvf ./transmission-*.tgz
cd transmission
nano values.yamls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
image:
repository: tccr.io/truecharts/transmission
pullPolicy: IfNotPresent
tag: v4.0.3@sha256:245158e56dae5ca2da2cac5e9e85d4879685e8f302ed955ba144162d504307e4

service:
main:
ports:
main:
port: WEBGUI_PORT_NUMBER
torrent:
enabled: true
ports:
torrent:
enabled: true
port: TRANSMISSION_PORT
protocol: tcp
torrentudp:
enabled: true
port: TRANSMISSION_PORT
protocol: udp

workload:
main:
podSpec:
containers:
main:
probes:
liveness:
enabled: true
type: tcp
startup:
enabled: true
type: tcp
readiness:
enabled: true
type: tcp
env:
# PUID: 1001
# URL is set here so it wont be able to get overwritten by the user
# as this will break the probes, if the need arises we can expose it.
TRANSMISSION__RPC_URL: "/transmission"
TRANSMISSION__RPC_USERNAME: "USERNAME"
TRANSMISSION__RPC_PASSWORD: "RANDOM_PASSWORD"
TRANSMISSION__RPC_AUTHENTICATION_REQUIRED: true
# TRANSMISSION__ALT_SPEED_DOWN: 50
# TRANSMISSION__ALT_SPEED_ENABLED: false
# TRANSMISSION__ALT_SPEED_TIME_BEGIN: 540
# TRANSMISSION__ALT_SPEED_TIME_DAY: 127
# TRANSMISSION__ALT_SPEED_TIME_ENABLED: false
# TRANSMISSION__ALT_SPEED_TIME_END: 1020
# TRANSMISSION__ALT_SPEED_UP: 50
# TRANSMISSION__BIND_ADDRESS_IPV4: "0.0.0.0"
# TRANSMISSION__BIND_ADDRESS_IPV6: "::"
# TRANSMISSION__BLOCKLIST_ENABLED: true
# TRANSMISSION__BLOCKLIST_URL: "https://github.com/Naunter/BT_BlockLists/releases/download/v.1/bt_blocklists.gz"
TRANSMISSION__CACHE_SIZE_MB: 4
# TRANSMISSION__DHT_ENABLED: true
TRANSMISSION__DOWNLOAD_DIR: "/data/Completed"
# TRANSMISSION__DOWNLOAD_QUEUE_ENABLED: true
# TRANSMISSION__DOWNLOAD_QUEUE_SIZE: 5
# TRANSMISSION__ENCRYPTION: 1
# TRANSMISSION__IDLE_SEEDING_LIMIT: 30
# TRANSMISSION__IDLE_SEEDING_LIMIT_ENABLED: false
TRANSMISSION__INCOMPLETE_DIR: "/data/Incomplete"
# TRANSMISSION__INCOMPLETE_DIR_ENABLED: true
# TRANSMISSION__LPD_ENABLED: false
# TRANSMISSION__MESSAGE_LEVEL: 2
# TRANSMISSION__PEER_CONGESTION_ALGORITHM: ""
# TRANSMISSION__PEER_ID_TTL_HOURS: 6
# TRANSMISSION__PEER_LIMIT_GLOBAL: 200
# TRANSMISSION__PEER_LIMIT_PER_TORRENT: 50
TRANSMISSION__PEER_PORT: "{{ .Values.service.torrent.ports.torrent.port }}"
# TRANSMISSION__PEER_PORT_RANDOM_HIGH: 65535
# TRANSMISSION__PEER_PORT_RANDOM_LOW: 49152
# TRANSMISSION__PEER_PORT_RANDOM_ON_START: false
# TRANSMISSION__PEER_SOCKET_TOS: default"
# TRANSMISSION__PEX_ENABLED: true
# TRANSMISSION__PORT_FORWARDING_ENABLED: false
# TRANSMISSION__PREALLOCATION: 1
# TRANSMISSION__PREFETCH_ENABLED: true
# TRANSMISSION__QUEUE_STALLED_ENABLED: true
# TRANSMISSION__QUEUE_STALLED_MINUTES: 30
# TRANSMISSION__RATIO_LIMIT: 2
# TRANSMISSION__RATIO_LIMIT_ENABLED: false
# TRANSMISSION__RENAME_PARTIAL_FILES: true
# TRANSMISSION__RPC_BIND_ADDRESS: "0.0.0.0"
# TRANSMISSION__RPC_ENABLED: true
# TRANSMISSION__RPC_HOST_WHITELIST: ""
# TRANSMISSION__RPC_HOST_WHITELIST_ENABLED: false
TRANSMISSION__RPC_PORT: "{{ .Values.service.main.ports.main.port }}"
# TRANSMISSION__RPC_WHITELIST: ""
# TRANSMISSION__RPC_WHITELIST_ENABLED: false
# TRANSMISSION__SCRAPE_PAUSED_TORRENTS_ENABLED: true
# TRANSMISSION__SCRIPT_TORRENT_DONE_ENABLED: false
# TRANSMISSION__SCRIPT_TORRENT_DONE_FILENAME: ""
# TRANSMISSION__SEED_QUEUE_ENABLED: false
# TRANSMISSION__SEED_QUEUE_SIZE: 10
# TRANSMISSION__SPEED_LIMIT_DOWN: 100
# TRANSMISSION__SPEED_LIMIT_DOWN_ENABLED: false
# TRANSMISSION__SPEED_LIMIT_UP: 100
# TRANSMISSION__SPEED_LIMIT_UP_ENABLED: false
# TRANSMISSION__START_ADDED_TORRENTS: true
# TRANSMISSION__TRASH_ORIGINAL_TORRENT_FILES: false
# TRANSMISSION__UMASK: 2
# TRANSMISSION__UPLOAD_SLOTS_PER_TORRENT: 14
# TRANSMISSION__UTP_ENABLED: true
# TRANSMISSION__WATCH_DIR: "/watch"
# TRANSMISSION__WATCH_DIR_ENABLED: false

persistence:
config:
enabled: true
mountPath: "/config"
type: pvc
existingClaim: transmission-config-pvc
data:
enabled: true
mountPath: "/data"
type: pvc
existingClaim: transmission-data-pvc

portal:
open:
enabled: true

manifestManager:
enabled: false

Install the helm chart with our values.yaml file

1
microk8s helm install syncthing-server truecharts/syncthing --values ./values.yaml 

Check the status of the installed application.

1
2
3
4
5
microk8s status
microk8s kubectl describe pods
microk8s kubectl
microk8s kubectl logs
microk8s kubectl describe pods

Common Errors

Error from server (BadRequest): container in pod is waiting to start: ContainerCreating
You probably need to change the permissions on the PV directory. This path is what is written in the PersistentVolume in the path variable. A quick chmod -R 777 to this path will most likely fix the issue. The conatiner should update the permissions once it runs.

Service yaml file

We need to expose the service to the outside world.
Thankfully microk8s has a built in loadbalancer called metallb

WEBGUI_PORT_NUMBER: The port you wish to run the webgui on
TRANSMISSION_PORT: The port Transmission uses to comminicate with external parties.

transmission-service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Service
metadata:
name: transmission-service
spec:
type: LoadBalancer
selector:
app.kubernetes.io/name: transmission
ports:
- name: transmission-tcp
protocol: TCP
port: TRANSMISSION_PORT
targetPort: TRANSMISSION_PORT
- name: transmission-udp
protocol: UDP
port: TRANSMISSION_PORT
targetPort: TRANSMISSION_PORT
- name: transmission-rpc-tcp
protocol: TCP
port: WEBGUI_PORT_NUMBER # web gui port
targetPort: WEBGUI_PORT_NUMBER
externalIPs:
- A.B.C.D

More information here
https://medium.com/swlh/kubernetes-external-ip-service-type-5e5e9ad62fcd

Apply the service

1
microk8s kubectl apply -f ./transmission-service.yaml 

Confirm the service is active

1
microk8s kubectl get services transmission-service
1
2
NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)                                           AGE
transmission-service LoadBalancer A.B.C.D X.Y.Z.A TRANSMISSION_PORT:TRANSMISSION_PORT_2/TCP,TRANSMISSION_PORT:TRANSMISSION_PORT_2 /UDP,WEBGUI_PORT_NUMBER:31056/TCP 9h

Firewall Rules

This assumes you are using ufw.
ufw is bascally a wrapper for IPTABLES. If you have ever used IPTABLES before you understand why ufw exists.
https://docs.syncthing.net/users/firewall.html

1
2
3
4
5
6
7
8

sudo ufw default allow routed

sudo ufw allow from A.B.C.0/24 to any port WEBGUI_PORT_NUMBER proto tcp
sudo ufw allow from E.F.G.H/24 to any port TRANSMISSION_PORT proto tcp
sudo ufw allow from E.F.G.H/24 to any port TRANSMISSION_PORT proto udp

sudo ufw status

References

https://kubernetes.io/docs/concepts/services-networking/service/

MQTT K8s Setup on Ubuntu 20.04

Install the k8s cluster

1
2
microk8s enable dns storage helm3
microk8s status

Pull the Mqtt helm chart

1
2
microk8s helm repo add truecharts https://charts.truecharts.org/
microk8s helm pull truecharts/mosquitto --version 8.0.11

Modify the values.yaml file to set the configuration

We’re going to set the mosquitto configuration within the container.

1
2
3
tar -xvf ./mosquitto-8.0.11.tgz
cd mosquitto
cat values.yamls

Bottom of values.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
persistence:
data:
enabled: true
mountPath: "/mosquitto/data"
configinc:
enabled: true
mountPath: "/mosquitto/configinc"
mosquitto-config:
enabled: "true"
mountPath: "/mosquitto/config/mosquitto.conf"
subPath: "mosquitto.conf"
type: "custom"
volumeSpec:
configMap:
name: '{{ template "tc.common.names.fullname" . }}-config'

mosquitto-config is a config map.
configinc and data are both directories on the host machine.

Make the data directory

1
2
3
4
5
mkdir /mnt/Poseidon/k8s/mosquitto
mkdir /mnt/Poseidon/k8s/mosquitto/data
mkdir /mnt/Poseidon/k8s/mosquitto/configinc

chown -R 777 /mnt/Poseidon/k8s/mosquitto

Create Persistent Volumes and Persistent Volume Claims

Persistent Volumes

configinc

HOSTNAME: example_host
FILE_PATH: /path/to/files/

mosquitto-configinc-pv.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: mosquitto-configinc-pv
spec:
capacity:
storage: 3Ti
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /path/to/files/mosquitto/configinc # This must exist on the host
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- example_host

microk8s kubectl apply -f mosquitto-configinc-pv.yaml

1
persistentvolume/mosquitto-configinc-pv created

Data

HOSTNAME: example_host

mosquitto-data-pv.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: mosquitto-data-pv
spec:
capacity:
storage: 3Ti
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /path/to/files/mosquitto/data # This must exist on the host
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- example_host

microk8s kubectl apply -f mosquitto-data-pv.yaml

1
persistentvolume/mosquitto-data-pv created

Verify PV

microk8s kubectl get pv

1
2
3
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                                 STORAGECLASS        REASON   AGE
mosquitto-configinc-pv 3Ti RWO Retain Available local-storage 97s
mosquitto-data-pv 3Ti RWO Retain Available local-storage 35s

Persistent Volume Claims

configinc

mosquitto-configinc-pvc.yaml

mosquitto-configinc-pvc.yaml
Bonds to mosquitto-configinc-pv

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mosquitto-configinc-pvc
spec:
storageClassName: local-storage # Empty string must be explicitly set otherwise default StorageClass will be set
accessModes:
- ReadWriteOnce
volumeName: mosquitto-configinc-pv
resources:
requests:
storage: 3Ti

microk8s kubectl apply -f mosquitto-configinc-pvc.yaml

Data

mosquitto-data-pvc.yaml
Bonds to mosquitto-data-pv

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mosquitto-data-pvc
spec:
storageClassName: local-storage # Empty string must be explicitly set otherwise default StorageClass will be set
accessModes:
- ReadWriteOnce
volumeName: mosquitto-data-pv
resources:
requests:
storage: 3Ti

microk8s kubectl apply -f mosquitto-data-pvc.yaml

Verify PV

microk8s kubectl get pvc

1
2
mosquitto-configinc-pvc       Bound    mosquitto-configinc-pv                     3Ti        RWO            local-storage       4m44s
mosquitto-data-pvc Bound mosquitto-data-pv 3Ti RWO local-storage 2m7s

Update the values file

We know need to tell the helm chart to mount our directories

1
nano values.yamls

Update the persistence section as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
image:
repository: tccr.io/truecharts/eclipse-mosquitto
tag: 2.0.15@sha256:9e1fbb32ae27aaaf18432ff4e7e046c54fda8a630851dadf0c332a94e81fbf67
pullPolicy: IfNotPresent

service:
main:
ports:
main:
port: 1883
targetPort: 1883
websockets:
enabled: true
ports:
websockets:
enabled: true
port: 9001
targetPort: 9001

ingress:
websockets:
autoLink: true

auth:
# -- By enabling this, `allow_anonymous` gets set to `false` in the mosquitto config.
enabled: false

websockets:
# -- By enabling this, an additional listener with protocol websockets is added in the mosquitto config.
enabled: false

configmap:
config:
enabled: true
data:
mosquitto.conf: |
listener {{ .Values.service.main.ports.main.targetPort }}
{{- if .Values.websockets.enabled }}
listener {{ .Values.service.websockets.ports.websockets.targetPort }}
protocol websockets
{{- end }}
{{- if .Values.auth.enabled }}
allow_anonymous false
{{- else }}
allow_anonymous true
{{- end }}
{{- if .Values.persistence.data.enabled }}
persistence true
persistence_location {{ .Values.persistence.data.mountPath }}
autosave_interval 1800
{{- end }}
{{- if .Values.persistence.configinc.enabled }}
include_dir {{ .Values.persistence.configinc.mountPath }}
{{- end }}

persistence:
data:
enabled: true
mountPath: "/mosquitto/data"
type: pvc
existingClaim: mosquitto-data-pvc
configinc:
enabled: true
mountPath: "/mosquitto/configinc"
type: pvc
existingClaim: mosquitto-configinc-pvc
mosquitto-config:
enabled: "true"
mountPath: "/mosquitto/config/mosquitto.conf"
subPath: "mosquitto.conf"
type: "custom"
volumeSpec:
configMap:
name: '{{ template "tc.common.names.fullname" . }}-config'

portal:
enabled: false

Install the helm chart

1
microk8s helm install mosquitto truecharts/mosquitto --version 8.0.11 --values ./values.yaml 

Check the status of the installed application.

1
2
3
4
5
microk8s status
microk8s kubectl show pods
microk8s kubectl
microk8s kubectl logs
microk8s kubectl describe pods

Common Errors

Error from server (BadRequest): container in pod is waiting to start: ContainerCreating
You probably need to change the permissions on the PV directory. This path is what is written in the PersistentVolume in the path varaibale. A quick chmod -R 777 to this path will most likely fix the issue. The conatiner should update the permissions once it runs.

Service yaml file

We need to expose the service to the outside world.
Thankfully microk8s has a built in loadbalancer called metallb

Replace Y with the MQTT port number. Default 1883
Replace Z with the MQTT API number Default 9001

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Service
metadata:
name: mqtt-service
spec:
type: LoadBalancer
selector:
app.kubernetes.io/name: mosquitto
ports:
- name: http
protocol: TCP
port: Y
targetPort: Y
- name: https
protocol: TCP
port: Z
targetPort: Z
externalIPs:
- X.X.X.X

More information here
https://medium.com/swlh/kubernetes-external-ip-service-type-5e5e9ad62fcd

Apply the service

1
microk8s kubectl apply -f ./mqtt-service.yaml 

Confirm the service is active

1
microk8s kubectl describe services mqtt-service

Local test client

You can test the connection locally on the server with this simple CLI mqtt client
Test client
https://mqttx.app/cli

Firewall Rules

This assumes you are using ufw.
ufw is bascally a wrapper for IPTABLES. If you have ever used IPTABLES before you understand why ufw exists.

1
2
3
sudo ufw default allow routed 
sudo ufw allow from X.X.X.0/X to any port Y proto tcp
sudo ufw status

Test external Connections

Once again try to connect to port Y with https://mqttx.app/cli

References

https://kubernetes.io/docs/concepts/services-networking/service/

Microscope Camera

Camera Selection

5MP camera is the min. Cellphone adapter is also available but I want a dedicated camera.

Research

This was a similar project to what I am trying to build. Very nice blog as well
https://www.briandorey.com/post/raspberry-pi-high-quality-camera-on-the-microscope

Hardware

Raspberrypi High Quaility Camera 12 MP C mount
https://www.adafruit.com/product/4561

The framerate on a raspberry pi is very low so I perchased an arducam camera to usb board.
https://www.arducam.com/product/arducam-uvc-camera-adapter-board-for-12mp-imx477-raspberry-pi-hq-camera/
https://www.amazon.com/gp/product/B08NVG2CY4/ref=ppx_od_dt_b_asin_title_s00?ie=UTF8&psc=1
I got the idea to use the arducam adapter from https://www.briandorey.com/post/raspberry-pi-high-quality-camera-on-the-microscope

User Manual for the Pi camera adapter.

Arducam-UVC-Camera-Adapter-Board-for-12MP-IMX477-Raspberry-Pi-HQ-Camera-b0278.pdf UC-733_DIM.pdf

Raspberrypi High Quaility Camera 12 MP C mount

Components: Raspberrypi High Quaility Camera 12 MP C mount, ribbion cable, and Arducam UVC Camera Adapter Board.

Connecting the Arducam UVC Camera Adapter Board to the Raspberrypi High Quaility Camera

Mechanical

Camera Adapter Mount

I originally went with mounting the camera without any lenses. Using this Raspberry Pi HQ camera adapter 3d printed
https://www.printables.com/model/59927-raspberry-hq-camera-to-amscope-adapter

The adapter mounted to the camera port of my AmScope T720B Scope

Front-Side

Side

Using the above setup I was able to take this image with the camera.
This is a plant leaf from the Codiaeum variegatum at ~40x

Camera Issues

The camera sensor size causes an inherent magnification effect called crop factor. The field of view becomes reduced due to the this magnification effect. You can see this in the image above.
More information here
https://learn.adafruit.com/raspberry-pi-hq-camera-lenses/crop-factor
https://www.microscopeworld.com/p-3341-microscope-c-mount-field-of-view.aspx
https://www.microscopeworld.com/t-microscope_c-mounts.aspx

In order to correct for the inherent magnification effect an adapter was purchased.
From these sources I was able to find that 0.3x to 0.5x is about correct for my sensor size

Adapter lens selection

AmScope RU050 0.5X Reduction Lens for C-mount Cameras FMA050 Cmount to 23mm adapter. This fits my AmScope T720B Scope

https://www.ebay.com/itm/141950389592

Picture of dust taken with the 0.5x adapter. It’s hard to tell from the images above but the field of view is noticably larger.

Enclosure

To protect the hardware I designed an enclouse

Microscope_Camera_Enclosure-Lid.stl Microscope_Camera_Enclosure-Enclosure.stl Microscope_Camera_Enclosure.FCStd Microscope_Camera_Enclosure.FCStd1

Software

I had problems with Cheese constantly changing the exposure. Thankfully many have suffered from this issue before. I found this link that is specific to the raspberry pi but useful. https://hackernoon.com/polising-raspberry-pi-high-quality-camera-3z113u18
I had to use a more advanced pieace of software called qv4l2

To install on debian linux

sudo apt-get install qv4l2

References

https://www.edmundoptics.com/knowledge-center/application-notes/microscopy/understanding-microscopes-and-objectives/
https://www.microscopeworld.com/p-3341-microscope-c-mount-field-of-view.aspx
https://www.microscopeworld.com/t-microscope_c-mounts.aspx
https://www.briandorey.com/post/raspberry-pi-high-quality-camera-on-the-microscope

Microscope Selection

Research

https://www.reddit.com/r/microbiology/comments/q5h8vv/what_is_considered_a_great_microscope_in_todays/
https://www.edmundoptics.com/knowledge-center/application-notes/microscopy/understanding-microscopes-and-objectives/

Requirements

From the research above I was able to settle on the following requirements

  • Greater than 1000X
  • Kohler light source
  • infinity corrected optics
  • Camera port

Selection

Amscope T720B https://amscope.com/products/t720b-hc2

Infinity Corrected Optical System with High Resolution
Fully Coated Optics with Crystal Clear & Sharp Images
Precise Mechanical Control System
Reversed Nosepiece Design
Kohler Illumination System with Field Diaphragm for Lighting Control
30-Degree Inclined, 360-Degree Swiveling, Compensation Free Trinocular Head
Eight Magnification Levels: 40X, 80X, 100X, 200X, 400X, 800X, 1000X, 2000X
Intensity-Variable Transmitted LED Lighting System
Abbe Condenser with Iris Diaphragm and Filter Holder
Rack and Pinion Adjustment for Condenser
Low Position Coaxial Stage Movement Controlling Knobs
Dual Side Coaxial Coarse and Fine Focusing Control
Adjustable Interpupillary Distance
Adjustable Diopter on Eyepieces
Durable Cast Alloy Frame with Stain Resistant Enamel Finish
Four Infinity Plan Objectives Included
Two Pairs of Extreme Widefield Eyepieces Included (EWF10X & WF20X)
Quadruple, Reversed, Extra-Large Nosepiece with Wide, Knurled Grip for Easy Operation
Large Double Layer Mechanical Stage with Stain Resistant Coating
Upward Stage Limit Stop to Protect Objectives and Slides
Manufactured under ISO 9001 Quality Control Standards
Excellent Five (5) Year Factory Warranty

Specifications :
Optical System: infinity corrected
Nosepiece: reversed, ball bearing quadruple
Head: gemel type trinocular head, 30-degree inclined
Eyepiece: high eye-point eyepieces, WF10X22mm, WF20X
Objectives: infinity plan objective 4X, 10X, 40X (spring), 100X (spring, oil)
Focusing: low position coaxial focus system
Focusing Range: 1-3/16” (30mm)
Interpupillary Adjustment Range: 2-3/16” - 3” (55-75mm)
Mechanical Tube Length: 6-5/16” (160mm)
Mechanical Stage: 8.5” x 5.9” (216mm x 150mm)
Stage Traveling Range: 2.9” x 2” (75x50mm)
Focusing Rang: 0.95” (24mm)
Division of Fine Focusing: 0.00003935” (0.001mm)
Illuminator: Built-in Kohler LED illumination system
Condenser: N.A. 1.25 achromatic condenser
Illumination: Kohler, LED
Power Supply: 90V-240 wide voltage, CE certified
Built in measurement capabilities
Weight: 28 lbs

Packing List :
One Trinocular Compensation-Free Head
One Microscope Body with Frame, Base, and Kohler Illumination System
Four High Quality DIN Plan Achromatic Objectives: 4X, 10X, 40X and 100X
One Pair of Widefield Eyepieces: WF10X
One Pair of Widefield Eyepieces: WF20X
One Dust Cover
One HDMI camera
One HDMI cable
Immersion Oil
User’s Manual

Purchasing

There is no reason pay top dollar for a microscope. You can find massive discounts on refunbised microscopes on ebay!
https://www.ebay.com/itm/381546384234?hash=item58d5efcb6a:g:9qkAAOSwPXFcFU2K

References

https://storage.googleapis.com/software-download-d79bb.appspot.com/Manual%20Download%20Files/Manual%20Files/Compound/720%20Series_Manual_151006.pdf

WLED Degchi Lamp

While traveling in India I came across these nice looking tin lamps.

It is sometimes called a Degchi lamp. Since I am not a fan of open flames so leds will have to fill in the role of a candle. This build will be a bit rough. I will refine the lamp in later posts.

Parts

Controller

ESP8266

Esp8266 Huzzah

Esp8266 Huzzah documentation

Level shifter

GeeekPi 6Pack TXS0108E 8 Channel Logic Level Converter Bi-Directional High Speed Full Duplex Shifter 3.3V 5V for Arduino Raspberry Pi

LED Strip

Led Strip

Power supply

5v 10 amp power upply with barrel plug

Plug

Barrel plug

Software

WLED

wled

Binary

Wled binary

Flashing

You will need a 3.3v usb to serial adapter. An FTDI based usb to serial adapteris perfered.

https://docs.espressif.com/projects/esptool/en/latest/esp32/
Method 2 https://kno.wled.ge/basics/install-binary/

Connections

Connection between the usb to serial adapter and the ESP8266.

  • Logic Level set to 3.3V
  • ESP -> FTDI
  • TX -> RX
  • RX -> TX
  • VCC -> 5V
  • GND -> GND

Operating system permission workarounds

In order to write data to ttyUSB or other serieal ports you must be a member of the dialout group.

sudo usermod -a -G dialout your_user_name

Log out log in

Flash WLED onto the ESP

sudo python3 ./esptool.py -p /dev/ttyUSB0 write_flash 0x0 ./WLED_0.13.1_ESP8266.bin

Electronics

Schematic

Electronics Rough Fit

Playing around with the electronics to confirm that the design works as expected.







Everything is hooked up but the lights are not working.

Discovered that the OE pin on the TXS0108E needs to be pulled high to VA (ESP Logic Level High 3.3V)

Physical Construction

Lamp

Test Fit

Lets see how everything could fit inside the lamp. Who cares how it looks at the moment. We will polish it later.

Electronics Enclosure

FreeCad Model

Degchi_Lamp_Enclosure.FCStd

First atttempt to create a quick enclosure.

View of the enclosure lid.

Enclosure STLs

Degchi_Lamp_Enclosure-Degchi_Lamp_Electronics_Enclosure.stl

Enclosure Main Body

Degchi_Lamp_Enclosure-Degchi_Lamp_Electronics_Cover.stl

Enclosure Main Body

Enclosure Assembly

1 amp Fuse

All the electronics seem to fit. A more professional version will be created when I get the parts.

Forcing all the electronics into the enclosure.

Everything is coming together.

Power on testing

Led Mounts

The inside of the lamp is coated in a thick non conductive coating. For the time being the led strip is just placed inside the lamp body.

Connecting to WIFI

WLED 13.1 has some trouble connecting to wifi networks.

  • Set wifi control channel to a fixed channel. e.g 1

  • Change channel width to 20 Mhz

  • Bind to static ip in router

  • Add the same static ip in wled wifi settings

Results

I am not a fan of this very large black power cable. I will replace the power cord with USB-C.
After measuring the power usage of the lamp at peak load it looks like USB-C is a good option. Peak load 0.6 Amps
This will be covered in a follow up article.





TP Link U3T Ubuntu

This device has the rtl8812bu chipset and you willneed to do a little more work to get it working.
Thankfully there is a working driver available for it here: https://github.com/cilynx/rtl88x2bu

To get it working, you will need to first install some packages and check out the Git repo:

sudo apt-get install build-essential dkms git
git clone https://github.com/cilynx/rtl88x2bu.git

Then follow the instructions here to install the driver:

cd rtl88x2bu
VER=$(sed -n 's/\PACKAGE_VERSION="\(.*\)"/\1/p' dkms.conf)
sudo rsync -rvhP ./ /usr/src/rtl88x2bu-${VER}
sudo dkms add -m rtl88x2bu -v ${VER}
sudo dkms build -m rtl88x2bu -v ${VER}
sudo dkms install -m rtl88x2bu -v ${VER}
sudo modprobe 88x2bu

References

Ask Ubuntu

Rigol DS1052E Oscilloscope Encoder Repair

Broken encoder

I dropped my trusty Rigol scope off of the table while testing. The trigger encoder knob broke off.

Replacement part

After a bit of googling I was able to locate the part number. This is a very popular scope with hobbyist so this information was not all that hard to find.

Opening the case

The screws that hold the case on are Trox or star drive. There are 6 screws. Two screws on the bottom near the feet, two screws under the handle, and two screws on either side of the power socket.

WARNING: Do not forget to remove the power button. If you try the case with the power button still in place the switch will snap off. The power button can be removed by pulling it upwards.

You will need an extension bit to get at the screws under the handle

Do not forget about the screws on the side

Back case removed

Once the case is removed, unscrew the standoffs on either side of the serial interface (DB9).
Lift off the metal rf sheild

You will need to remove all the screws inside of the case. All The power supply board must be removed
Disconnect the power supply board. Watch out for the LCD lamp power cable (Red/White cable with JST connector)

You will need to disconnect the white ribbion cable from the board at the bottom of the unit.

The front case panel can now be removed.

Power supply board. Power switch

Front case panel removed. Picture of the 3 screws holding on the user control board.
These will need to be removed.

Replacing the encoder

Boken encoder next to replacement encoder.

Bottom of the user control board. Unsoldering required.
WARNING: Rigol uses lead free solder. Only use lead free solder. If you mix leaded solder and lead free solder a new alloy with a higher melting point will be formed. Good luck removing that!

Desoldered encoder

Replacement encoder

Put everything back together