<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-03-26T03:55:16+00:00</updated><id>/feed.xml</id><title type="html">Casper.im</title><subtitle>Just a tech blog</subtitle><author><name>Casper</name></author><entry><title type="html">Dual Radio Meshtastic Node</title><link href="/Dual-Radio-Meshtasticd/" rel="alternate" type="text/html" title="Dual Radio Meshtastic Node" /><published>2025-12-28T00:00:00+00:00</published><updated>2025-12-28T20:00:00+00:00</updated><id>/Dual-Radio-Meshtasticd</id><content type="html" xml:base="/Dual-Radio-Meshtasticd/"><![CDATA[<p><img src="/images/posts/nebra/nebra-dual.jpg" alt="Meshtastic Pi" /></p>

<h3 id="hardware-install">Hardware Install</h3>

<p>This instance is a Hooper ZebraHat and a prototype USB meshtastic radio that’s picked up by default. You’ll have to adjust each. The script does create two folders under /etc/meshtasticd to make life easier. It works pretty much exactly like the base folder.</p>

<p>You shouldn’t need to adjust the systemctl unit files but you can do so by finding the files with ls /etc/systemd/system/meshtasticd*</p>

<p>This was developed from <a href="https://platfrastructure.life/post/meshtasticd-multi/">platfrastructure</a></p>

<h3 id="setup-script">Setup Script</h3>

<p>This is brand new and I’m still working through the config. Please reach out if you run into any issues.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="c"># --- Instances ---</span>
<span class="nv">ZEBRA_NAME</span><span class="o">=</span><span class="s2">"zebra"</span>
<span class="nv">USB_NAME</span><span class="o">=</span><span class="s2">"usb"</span>

<span class="nv">ZEBRA_PORT</span><span class="o">=</span><span class="s2">"4403"</span>
<span class="nv">USB_PORT</span><span class="o">=</span><span class="s2">"4404"</span>

<span class="nv">BASE_CFG</span><span class="o">=</span><span class="s2">"/etc/meshtasticd/config.yaml"</span>

<span class="nv">ZEBRA_DIR</span><span class="o">=</span><span class="s2">"/etc/meshtasticd/</span><span class="k">${</span><span class="nv">ZEBRA_NAME</span><span class="k">}</span><span class="s2">"</span>
<span class="nv">USB_DIR</span><span class="o">=</span><span class="s2">"/etc/meshtasticd/</span><span class="k">${</span><span class="nv">USB_NAME</span><span class="k">}</span><span class="s2">"</span>

<span class="nv">ZEBRA_CFG</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">ZEBRA_DIR</span><span class="k">}</span><span class="s2">/config.yaml"</span>
<span class="nv">USB_CFG</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">USB_DIR</span><span class="k">}</span><span class="s2">/config.yaml"</span>

<span class="nv">ZEBRA_CFGD</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">ZEBRA_DIR</span><span class="k">}</span><span class="s2">/config.d"</span>
<span class="nv">USB_CFGD</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">USB_DIR</span><span class="k">}</span><span class="s2">/config.d"</span>

<span class="nv">ZEBRA_YAML</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">ZEBRA_CFGD</span><span class="k">}</span><span class="s2">/ZebraHat.yaml"</span>
<span class="nv">ZEBRA_YAML_URL</span><span class="o">=</span><span class="s2">"https://raw.githubusercontent.com/wehooper4/Meshtastic-Hardware/refs/heads/main/ZebraHAT/ZebraHat.yaml"</span>

<span class="nv">ZEBRA_FSDIR</span><span class="o">=</span><span class="s2">"/var/lib/meshtasticd-</span><span class="k">${</span><span class="nv">ZEBRA_NAME</span><span class="k">}</span><span class="s2">"</span>
<span class="nv">USB_FSDIR</span><span class="o">=</span><span class="s2">"/var/lib/meshtasticd-</span><span class="k">${</span><span class="nv">USB_NAME</span><span class="k">}</span><span class="s2">"</span>

<span class="nv">ZEBRA_UNIT</span><span class="o">=</span><span class="s2">"/etc/systemd/system/meshtasticd-</span><span class="k">${</span><span class="nv">ZEBRA_NAME</span><span class="k">}</span><span class="s2">.service"</span>
<span class="nv">USB_UNIT</span><span class="o">=</span><span class="s2">"/etc/systemd/system/meshtasticd-</span><span class="k">${</span><span class="nv">USB_NAME</span><span class="k">}</span><span class="s2">.service"</span>

<span class="nv">MESHTASTICD_BIN</span><span class="o">=</span><span class="s2">"/usr/bin/meshtasticd"</span>

<span class="c"># --- Helper: generate random MAC like AA:BB:CC:DD:EE:FF (random last 3 bytes) ---</span>
rand_mac<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span>r
  <span class="nv">r</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>openssl rand <span class="nt">-hex</span> 3<span class="si">)</span><span class="s2">"</span>              <span class="c"># 6 hex chars</span>
  <span class="c"># Format: AA:BB:CC:xx:yy:zz</span>
  <span class="nb">printf</span> <span class="s2">"AA:BB:CC:%s:%s:%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">r</span>:0:2<span class="k">}</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">r</span>:2:2<span class="k">}</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">r</span>:4:2<span class="k">}</span><span class="s2">"</span>
<span class="o">}</span>

<span class="c"># --- Helper: patch config.yaml (YAML indentation safe) ---</span>
patch_config<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span><span class="nv">cfg</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
  <span class="nb">local </span><span class="nv">cfgdir</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>
  <span class="nb">local </span><span class="nv">mac</span><span class="o">=</span><span class="s2">"</span><span class="nv">$3</span><span class="s2">"</span>

  <span class="c"># 1) Point ConfigDirectory at instance-specific config.d (keep YAML indentation)</span>
  <span class="c">#    Replace any existing ConfigDirectory line (commented or not).</span>
  <span class="k">if </span><span class="nb">grep</span> <span class="nt">-qE</span> <span class="s1">'^[[:space:]]*ConfigDirectory:'</span> <span class="s2">"</span><span class="nv">$cfg</span><span class="s2">"</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">sed</span> <span class="nt">-i</span> <span class="nt">-E</span> <span class="s2">"s|^[[:space:]]*ConfigDirectory:[[:space:]]*.*</span><span class="nv">$|</span><span class="s2">  ConfigDirectory: </span><span class="k">${</span><span class="nv">cfgdir</span><span class="k">}</span><span class="s2">/|"</span> <span class="s2">"</span><span class="nv">$cfg</span><span class="s2">"</span>
  <span class="k">else</span>
    <span class="c"># Insert after "General:" line</span>
    <span class="nb">awk</span> <span class="nt">-v</span> <span class="nv">cfgdir</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">cfgdir</span><span class="k">}</span><span class="s2">"</span> <span class="s1">'
      {print}
      /^General:[[:space:]]*$/ {print "  ConfigDirectory: " cfgdir "/"}
    '</span> <span class="s2">"</span><span class="nv">$cfg</span><span class="s2">"</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="k">${</span><span class="nv">cfg</span><span class="k">}</span><span class="s2">.tmp"</span> <span class="o">&amp;&amp;</span> <span class="nb">mv</span> <span class="s2">"</span><span class="k">${</span><span class="nv">cfg</span><span class="k">}</span><span class="s2">.tmp"</span> <span class="s2">"</span><span class="nv">$cfg</span><span class="s2">"</span>
  <span class="k">fi</span>

  <span class="c"># 2) Comment out MACAddressSource: eth0 with YAML-safe indentation:</span>
  <span class="c">#    "  MACAddressSource: eth0"  -&gt; "  # MACAddressSource: eth0"</span>
  <span class="c">#    If it's already commented, leave it alone.</span>
  <span class="nb">sed</span> <span class="nt">-i</span> <span class="nt">-E</span> <span class="s1">'s/^([[:space:]]*)MACAddressSource:[[:space:]]*eth0/\1# MACAddressSource: eth0/'</span> <span class="s2">"</span><span class="nv">$cfg</span><span class="s2">"</span>

  <span class="c"># 3) Ensure MACAddress is set with EXACTLY two leading spaces:</span>
  <span class="c">#    Replace existing commented/uncommented MACAddress line, anywhere.</span>
  <span class="k">if </span><span class="nb">grep</span> <span class="nt">-qE</span> <span class="s1">'^[[:space:]]*#?[[:space:]]*MACAddress:[[:space:]]*'</span> <span class="s2">"</span><span class="nv">$cfg</span><span class="s2">"</span><span class="p">;</span> <span class="k">then</span>
    <span class="c"># Normalize to exactly "  MACAddress: &lt;mac&gt;"</span>
    <span class="nb">sed</span> <span class="nt">-i</span> <span class="nt">-E</span> <span class="s2">"s|^[[:space:]]*#?[[:space:]]*MACAddress:[[:space:]]*.*</span><span class="nv">$|</span><span class="s2">  MACAddress: </span><span class="k">${</span><span class="nv">mac</span><span class="k">}</span><span class="s2">|"</span> <span class="s2">"</span><span class="nv">$cfg</span><span class="s2">"</span>
  <span class="k">else</span>
    <span class="c"># Insert after "General:" line</span>
    <span class="nb">awk</span> <span class="nt">-v</span> <span class="nv">mac</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">mac</span><span class="k">}</span><span class="s2">"</span> <span class="s1">'
      {print}
      /^General:[[:space:]]*$/ {print "  MACAddress: " mac}
    '</span> <span class="s2">"</span><span class="nv">$cfg</span><span class="s2">"</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="k">${</span><span class="nv">cfg</span><span class="k">}</span><span class="s2">.tmp"</span> <span class="o">&amp;&amp;</span> <span class="nb">mv</span> <span class="s2">"</span><span class="k">${</span><span class="nv">cfg</span><span class="k">}</span><span class="s2">.tmp"</span> <span class="s2">"</span><span class="nv">$cfg</span><span class="s2">"</span>
  <span class="k">fi</span>
<span class="o">}</span>

<span class="c"># --- Preconditions ---</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nv">$EUID</span> <span class="nt">-ne</span> 0 <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"ERROR: Run as root (sudo)."</span>
  <span class="nb">exit </span>1
<span class="k">fi

if</span> <span class="o">[[</span> <span class="o">!</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$BASE_CFG</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"ERROR: Base config not found at </span><span class="nv">$BASE_CFG</span><span class="s2">"</span>
  <span class="nb">exit </span>1
<span class="k">fi

if</span> <span class="o">[[</span> <span class="o">!</span> <span class="nt">-x</span> <span class="s2">"</span><span class="nv">$MESHTASTICD_BIN</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"ERROR: meshtasticd not found/executable at </span><span class="nv">$MESHTASTICD_BIN</span><span class="s2">"</span>
  <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># --- STOP/DISABLE ORIGINAL SINGLE-INSTANCE SERVICE (no prompt) ---</span>
<span class="k">if </span>systemctl list-unit-files | <span class="nb">grep</span> <span class="nt">-qE</span> <span class="s1">'^meshtasticd\.service'</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"Stopping/disabling original meshtasticd.service ..."</span>
  systemctl stop meshtasticd.service <span class="o">||</span> <span class="nb">true
  </span>systemctl disable meshtasticd.service <span class="o">||</span> <span class="nb">true
  </span>systemctl mask meshtasticd.service <span class="o">||</span> <span class="nb">true
</span><span class="k">fi</span>

<span class="c"># --- Create directories ---</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$ZEBRA_CFGD</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_CFGD</span><span class="s2">"</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$ZEBRA_FSDIR</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_FSDIR</span><span class="s2">"</span>

<span class="c"># --- Copy base config.yaml into each instance directory ---</span>
<span class="nb">cp</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$BASE_CFG</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$ZEBRA_CFG</span><span class="s2">"</span>
<span class="nb">cp</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$BASE_CFG</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_CFG</span><span class="s2">"</span>

<span class="c"># --- Download ZebraHat.yaml into zebra instance config.d ---</span>
wget <span class="nt">-q</span> <span class="nt">-O</span> <span class="s2">"</span><span class="nv">$ZEBRA_YAML</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$ZEBRA_YAML_URL</span><span class="s2">"</span>

<span class="c"># --- Patch both instance configs ---</span>
<span class="nv">ZEBRA_MAC</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>rand_mac<span class="si">)</span><span class="s2">"</span>
<span class="nv">USB_MAC</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>rand_mac<span class="si">)</span><span class="s2">"</span>

patch_config <span class="s2">"</span><span class="nv">$ZEBRA_CFG</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$ZEBRA_CFGD</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$ZEBRA_MAC</span><span class="s2">"</span>
patch_config <span class="s2">"</span><span class="nv">$USB_CFG</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_CFGD</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_MAC</span><span class="s2">"</span>

<span class="c"># --- Determine whether meshtasticd user exists ---</span>
<span class="nv">USE_USERGROUP</span><span class="o">=</span><span class="s2">"yes"</span>
<span class="k">if</span> <span class="o">!</span> <span class="nb">id </span>meshtasticd <span class="o">&gt;</span>/dev/null 2&gt;&amp;1<span class="p">;</span> <span class="k">then
  </span><span class="nv">USE_USERGROUP</span><span class="o">=</span><span class="s2">"no"</span>
  <span class="nb">echo</span> <span class="s2">"WARN: user 'meshtasticd' not found; unit files will run as root."</span>
<span class="k">fi</span>

<span class="c"># --- Write unit files (multi-instance pattern: --port, --config, --fsdir) ---</span>
<span class="nb">cat</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="nv">$ZEBRA_UNIT</span><span class="s2">"</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
[Unit]
Description=Meshtastic Native Daemon (</span><span class="k">${</span><span class="nv">ZEBRA_NAME</span><span class="k">}</span><span class="sh">)
After=network-online.target
StartLimitInterval=200
StartLimitBurst=5

[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
Type=simple
</span><span class="no">EOF

</span><span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$USE_USERGROUP</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"yes"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">cat</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$ZEBRA_UNIT</span><span class="s2">"</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
User=meshtasticd
Group=meshtasticd
</span><span class="no">EOF
</span><span class="k">fi

</span><span class="nb">cat</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$ZEBRA_UNIT</span><span class="s2">"</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
ExecStart=</span><span class="k">${</span><span class="nv">MESHTASTICD_BIN</span><span class="k">}</span><span class="sh"> --port </span><span class="k">${</span><span class="nv">ZEBRA_PORT</span><span class="k">}</span><span class="sh"> --config </span><span class="k">${</span><span class="nv">ZEBRA_CFG</span><span class="k">}</span><span class="sh"> --fsdir </span><span class="k">${</span><span class="nv">ZEBRA_FSDIR</span><span class="k">}</span><span class="sh">
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
</span><span class="no">EOF

</span><span class="nb">cat</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="nv">$USB_UNIT</span><span class="s2">"</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
[Unit]
Description=Meshtastic Native Daemon (</span><span class="k">${</span><span class="nv">USB_NAME</span><span class="k">}</span><span class="sh">)
After=network-online.target
StartLimitInterval=200
StartLimitBurst=5

[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
Type=simple
</span><span class="no">EOF

</span><span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$USE_USERGROUP</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"yes"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">cat</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$USB_UNIT</span><span class="s2">"</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
User=meshtasticd
Group=meshtasticd
</span><span class="no">EOF
</span><span class="k">fi

</span><span class="nb">cat</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$USB_UNIT</span><span class="s2">"</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
ExecStart=</span><span class="k">${</span><span class="nv">MESHTASTICD_BIN</span><span class="k">}</span><span class="sh"> --port </span><span class="k">${</span><span class="nv">USB_PORT</span><span class="k">}</span><span class="sh"> --config </span><span class="k">${</span><span class="nv">USB_CFG</span><span class="k">}</span><span class="sh"> --fsdir </span><span class="k">${</span><span class="nv">USB_FSDIR</span><span class="k">}</span><span class="sh">
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
</span><span class="no">EOF

</span><span class="c"># --- Permissions (fix /var/lib/meshtasticd-zebra and friends) ---</span>
<span class="c"># Ensure sane modes regardless of whether the user/group exists.</span>
<span class="nb">chmod </span>0755 <span class="s2">"</span><span class="nv">$ZEBRA_DIR</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_DIR</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$ZEBRA_CFGD</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_CFGD</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">true
chmod </span>0644 <span class="s2">"</span><span class="nv">$ZEBRA_CFG</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_CFG</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$ZEBRA_YAML</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">true
chmod </span>0750 <span class="s2">"</span><span class="nv">$ZEBRA_FSDIR</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_FSDIR</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">true

</span><span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$USE_USERGROUP</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"yes"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">chown</span> <span class="nt">-R</span> meshtasticd:meshtasticd <span class="s2">"</span><span class="nv">$ZEBRA_DIR</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_DIR</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">true
  chown</span> <span class="nt">-R</span> meshtasticd:meshtasticd <span class="s2">"</span><span class="nv">$ZEBRA_FSDIR</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USB_FSDIR</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">true
</span><span class="k">fi</span>

<span class="c"># --- Enable and start new instances ---</span>
systemctl daemon-reload
systemctl <span class="nb">enable</span> <span class="nt">--now</span> <span class="s2">"meshtasticd-</span><span class="k">${</span><span class="nv">ZEBRA_NAME</span><span class="k">}</span><span class="s2">.service"</span>
systemctl <span class="nb">enable</span> <span class="nt">--now</span> <span class="s2">"meshtasticd-</span><span class="k">${</span><span class="nv">USB_NAME</span><span class="k">}</span><span class="s2">.service"</span>

<span class="nb">echo
echo</span> <span class="s2">"✅ Created and started:"</span>
<span class="nb">echo</span> <span class="s2">"  meshtasticd-</span><span class="k">${</span><span class="nv">ZEBRA_NAME</span><span class="k">}</span><span class="s2">.service (port </span><span class="k">${</span><span class="nv">ZEBRA_PORT</span><span class="k">}</span><span class="s2">)"</span>
<span class="nb">echo</span> <span class="s2">"  meshtasticd-</span><span class="k">${</span><span class="nv">USB_NAME</span><span class="k">}</span><span class="s2">.service   (port </span><span class="k">${</span><span class="nv">USB_PORT</span><span class="k">}</span><span class="s2">)"</span>
<span class="nb">echo
echo</span> <span class="s2">"✅ Configs:"</span>
<span class="nb">echo</span> <span class="s2">"  </span><span class="nv">$ZEBRA_CFG</span><span class="s2">  (MACAddress: </span><span class="k">${</span><span class="nv">ZEBRA_MAC</span><span class="k">}</span><span class="s2">, ConfigDirectory: </span><span class="k">${</span><span class="nv">ZEBRA_CFGD</span><span class="k">}</span><span class="s2">/)"</span>
<span class="nb">echo</span> <span class="s2">"  </span><span class="nv">$USB_CFG</span><span class="s2">    (MACAddress: </span><span class="k">${</span><span class="nv">USB_MAC</span><span class="k">}</span><span class="s2">,   ConfigDirectory: </span><span class="k">${</span><span class="nv">USB_CFGD</span><span class="k">}</span><span class="s2">/)"</span>
<span class="nb">echo
echo</span> <span class="s2">"✅ ZebraHat.yaml:"</span>
<span class="nb">echo</span> <span class="s2">"  </span><span class="nv">$ZEBRA_YAML</span><span class="s2">"</span>
<span class="nb">echo
echo</span> <span class="s2">"Check status/logs:"</span>
<span class="nb">echo</span> <span class="s2">"  systemctl status meshtasticd-</span><span class="k">${</span><span class="nv">ZEBRA_NAME</span><span class="k">}</span><span class="s2">.service"</span>
<span class="nb">echo</span> <span class="s2">"  systemctl status meshtasticd-</span><span class="k">${</span><span class="nv">USB_NAME</span><span class="k">}</span><span class="s2">.service"</span>
<span class="nb">echo</span> <span class="s2">"  journalctl -u meshtasticd-</span><span class="k">${</span><span class="nv">ZEBRA_NAME</span><span class="k">}</span><span class="s2">.service -f"</span>
<span class="nb">echo</span> <span class="s2">"  journalctl -u meshtasticd-</span><span class="k">${</span><span class="nv">USB_NAME</span><span class="k">}</span><span class="s2">.service -f"</span>

</code></pre></div></div>

<h3 id="file-permissions">File Permissions</h3>

<p>You may need to update file permissions.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nb">sudo chown</span> <span class="nt">-R</span> meshtasticd:meshtasticd /var/lib/meshtasticd-zebra
<span class="nb">sudo chown</span> <span class="nt">-R</span> meshtasticd:meshtasticd /var/lib/meshtasticd-usb
<span class="nb">sudo chmod </span>750 /var/lib/meshtasticd-zebra
<span class="nb">sudo chmod </span>750 /var/lib/meshtasticd-usb
<span class="nb">sudo </span>find /var/lib/meshtasticd-zebra <span class="nt">-type</span> d <span class="nt">-exec</span> <span class="nb">chmod </span>750 <span class="o">{}</span> <span class="se">\;</span>
<span class="nb">sudo </span>find /var/lib/meshtasticd-usb   <span class="nt">-type</span> d <span class="nt">-exec</span> <span class="nb">chmod </span>750 <span class="o">{}</span> <span class="se">\;</span>

</code></pre></div></div>

<h3 id="config">Config</h3>

<p>Command line should make life easier. But you have to do them one by one, the cli is slow and clunky.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>meshtastic <span class="nt">--host</span> localhost:4403 <span class="nt">--set</span> lora.region <span class="s2">"US"</span>
meshtastic <span class="nt">--host</span> localhost:4403 <span class="nt">--set-owner</span> <span class="s2">"SUSQ VAL PA Mesh - Radio 1"</span>
meshtastic <span class="nt">--host</span> localhost:4403 <span class="nt">--set-owner-short</span> <span class="s2">"SVMI"</span>

meshtastic <span class="nt">--host</span> localhost:4404 <span class="nt">--set</span> lora.region <span class="s2">"US"</span>
meshtastic <span class="nt">--host</span> localhost:4404 <span class="nt">--set-owner</span> <span class="s2">"SUSQ VAL PA Mesh - Radio 2"</span>
meshtastic <span class="nt">--host</span> localhost:4404 <span class="nt">--set-owner-short</span> <span class="s2">"SVMI"</span>

<span class="nb">sudo </span>systemctl restart meshtasticd-zebra
<span class="nb">sudo </span>systemctl restart meshtasticd-usb

</code></pre></div></div>

<h3 id="networked-nodes">Networked Nodes</h3>

<p>These commands enables UDP communication between the nodes. For example, if you had two radios attached to yagi, directiona or sectorized antennas, and wanted to operate a retrans site.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
meshtastic <span class="nt">--host</span> localhost:4403 <span class="nt">-set</span> network.enable_protocols 1
meshtastic <span class="nt">--host</span> localhost:4404 <span class="nt">-set</span> network.enable_protocols 1
</code></pre></div></div>

<h3 id="troubleshooting">Troubleshooting</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
journalctl <span class="nt">-u</span> meshtasticd-usb.service
journalctl <span class="nt">-u</span> meshtasticd-zebra.service

</code></pre></div></div>]]></content><author><name>Casper</name></author><category term="Meshtastic" /><category term="LoRA" /><summary type="html"><![CDATA[Allows for one node to operate as a retrans site using two antennas, by using multiple networked instances of meshtasticd]]></summary></entry><entry><title type="html">Easy way to pass FCC Amateur Radio licenses</title><link href="/FCC-License-Test-Prep/" rel="alternate" type="text/html" title="Easy way to pass FCC Amateur Radio licenses" /><published>2025-12-13T00:00:00+00:00</published><updated>2025-12-13T20:00:00+00:00</updated><id>/FCC-License-Test-Prep</id><content type="html" xml:base="/FCC-License-Test-Prep/"><![CDATA[<h3 id="licenses-classes">Licenses classes</h3>

<p>Technician is intro license, you get a little of everything but it’s pretty limited. 
General gives you basically everything.
Extra gives you everything in general plus a couple of extra frequencies. Biggest gain is being able to be an examiner for all licenses and a short callsign.</p>

<p>The questions are known due to FOIA requests. There are no surprise questions.</p>

<p>Technician has 412 total questions and 35 categories, 26 needed to pass, good until 2026.
General has 432 total questions and 35 categories, 26 needed to pass, good until 2027.
Extra has 600 total questions and 50 categories, 37 needed to pass, good until 2028.</p>

<p>Only one question is asked per category. For Tech and General, you can get 9 answers wrong and still pass. Which means you can get 9 categories wrong.</p>

<h3 id="passing-the-exam">Passing the exam</h3>

<p>Knowing this, it’s pretty easy to min/max the test pretty easily. I recommend the Hamstudy.org app, which is $5. You can just use the web site but the mobile app is pretty decent. You can buy the book, but I honestly don’t recommend using it to study.</p>

<p>Register on the site, buy the app, install it, open it.</p>

<p>Click on Technician. Click on Study. Click on the gear icon in upper right hand corner. Hit Customization. Check ONE chapter and ONE subsection, and then hit apply. While on the Customization screen, I do recommend Auto Advance, Randomize Questions and Show Answer History. I do NOT recommend checking Randomize Answers. You’ll see the available number of questions.</p>

<h3 id="the-minmax-process">The Min/Max Process</h3>

<p>Start the questions. If you don’t know, hit “Don’t Know” rather than guessing. Guessing blindly can reinforce memory of wrong answers.</p>

<p>If you don’t understand the question, use the “Explain” in the upper right corner. They’re pretty handy and walk you through the thought process. You can also google individual questions. Normally I don’t recommend AI for much, but asking ChatGPT or other AI to explain the question and why the answer is the answer can be useful.</p>

<p>Here’s the trick. Repeat the individual chapter not until you get every question right, but rather until you can’t get any answer wrong. Multiple times in a row. Once I get every question right 3 or 4 times in a row, I move onto the subsection. Once you have all the sub-sections done, do the tests for the entire chapter. If you can do all the questions in the chapter two times in a row with perfect or near perfect score, move onto next chapter.</p>

<p>Now, remember, you can get up to 9 questions wrong and it’s 1 question per subsection. NOT every subsection will be on the test. If a section just won’t stick and you’re limited on time, SKIP IT. I wouldn’t skip more than a few subsections. Skipping 9 subsections is a bad idea. 2 or 3 isn’t. The test is pass/fail, your score doesn’t matter.</p>

<h3 id="when-youre-ready">When you’re ready</h3>

<p>Schedule your test once you consistantly score 80% on a couple practice exams.
Once you routinely score over 90%, you’re pretty much guaranteed to pass.</p>

<p>If you’re going for your Tech license, go through the General questions. You don’t have to memorize them or do the same level of prep if you don’t wish to. But you can take all of the exams in one sitting, and it’s the same $15 cost whether you take just the Technian or Technician and General. If you’re scoring a 25% on general practice test, you’re not going to pass the General. If you get around or over 50%, it might be worth putting in a bit of work and trying to pass both. If you don’t pass the General, you’re not out of anything. You can just take it later.</p>

<h3 id="scheduling-the-test">Scheduling the test</h3>

<p>You’ll want your FRN from the FCC. If you don’t have it, register for it. Bring it with you on testing day as well. Bring a pen and blank paper with you.</p>

<p>While you’re on the FCC site, register for your GMRS license. No test is required.</p>

<p>Go to hamstudy.org and login</p>

<p>Click on “Find a Session” button in upper right corner of the web site</p>

<p>Put in your zip code next to Located.</p>

<p>Find a location that works for you. Click on it, read any notes about the testing location. Answer any questions.</p>

<p>If given the choice between laptop and tablet, pick laptop.</p>

<h3 id="random-tips-and-tricks">Random Tips and Tricks</h3>

<p>Memorize ohm’s law, and sketch it on your blank paper. Include the terms and units of measure.</p>

<p>Light moves at 300,000,000 miles per second. So to move from frequency to short name, just divide the frequency by 300. 150Mhz is 2m, 30Mhz is 10m.</p>]]></content><author><name>Casper</name></author><category term="radio" /><category term="fcc" /><summary type="html"><![CDATA[Cram for the test, learn by doing]]></summary></entry><entry><title type="html">Meshtastic Tower Nodes</title><link href="/Tower-Nodes/" rel="alternate" type="text/html" title="Meshtastic Tower Nodes" /><published>2025-12-01T00:00:00+00:00</published><updated>2025-12-01T20:00:00+00:00</updated><id>/Tower-Nodes</id><content type="html" xml:base="/Tower-Nodes/"><![CDATA[<h3 id="basic-concept">Basic Concept</h3>

<p>The main advantage of a mesh is resiliance. The main disadvantage is inefficiency. Tower nodes can help with both sides. Height is might with line of sight radios.</p>

<h3 id="how-do-i-get-tower-permission">How do I get tower permission?</h3>

<p>I get asked this a lot. Very often by folks who are on the less experienced of radios.</p>

<p>Tower nodes are very difficult, very detail oriented, very expensive and everything has to work perfectly for years. You want to go the extra mile. Not for your equipment, for everyone else’s equipment. So you don’t damage someone else’s very expensive equipment. Oh, and your gear can all be fried by a near lightning strike at any time even if you do everything perfectly</p>

<p>Join your local ham club, volunteer for anything you can, make it generally clear you’re sane and trustworthy. If you’ve never put anything up on a tower, there’s a lot to learn and it’s best to learn from existing installs. Ask for your club for permissions and assistance, put one up under supervision. Preferably at ladder height. After you have a proven track record, it’s an easier sell to other tower owners, businesses and clubs. I waited until I was very comfortable and knew what I was doing before putting up in locations I didn’t own.</p>

<p>We go over the top to make sure it’s VERY clear to the tower owners that their property has my highest priority. We also don’t just ask for tower permission. We offer to do presentations, help out if the club needs or wants extra hobbies, try to get local folks to join the club, etc. Don’t just put up a node and ignore them. Word gets around, and at a certain point, they start beating on your door instead of you having to beat on their’s.</p>

<p>There is no magic shortcut.</p>

<h3 id="test-stand">Test Stand</h3>

<p>I recommend doing small scale testing at first. Get a pole somewhere, and mount your theoretical tower node on it. See how it survives the weather. Once a node is up a tower, it may be years before it’s touched so everything has to be perfect. We do iterate as we learn lessons, but everything needs testing. Try never to deploy an untested node. Every tower node I put up should have a minimum of a week on a test pole to verify everything is running the way it should.</p>

<p>My test stand is a concrete stand intended for a bird bath. I shove a fence pole into it and strap the tower node to it.</p>

<p>You generally want to secure your node with U-bolts. Worm-Drive hose clamps aren’t bad for home or building installs. I can see it as secondary or safety strap for a unit on a tower. Stainless steel banding with banding buckles is preferable but not cheap or easy.</p>

<h3 id="site-planning">Site Planning</h3>

<p>I start by doing radio propagation sims from existing towers. Look for good locations (high with lots of visibility) where there isn’t already a node. If you can, go there and do some live testing.</p>

<p>Generally you’re not looking for one spot, you’re looking for a ridge line. Look on google maps and tower sites to look for tower owners in that area.</p>

<h3 id="nebra-strut">Nebra Strut</h3>

<p>13 in and 23 in versions
Angle iron on both ends</p>

<p>For the strut:
one arm, centered on both sides,
centering keeps from interferring with antennas on either side of enclosure,</p>

<p>On the enclosure side:
9 inches long angle iron,
two mounting holes, centered width wise, 5/16 holes, 7.5 inches apart (190mm preferably),</p>

<p>On tower side:
9 inch long angle iron
two 1.5 inch U-bolts</p>

<p>maybe gusset plates ?</p>

<h3 id="grounding">Grounding</h3>

<p>Grounding is half math, and half religion. And the holy tome is the <a href="https://www.blm.gov/sites/blm.gov/files/Lands_ROW_Motorola_R56_2005_manual.pdf">Motorola R56</a>.</p>]]></content><author><name>Casper</name></author><category term="meshtastic" /><category term="LoRA" /><summary type="html"><![CDATA[Lessons learned from deploying meshtastic nodes up towers]]></summary></entry><entry><title type="html">Meshtastic with Luckfox Lyra Ultra</title><link href="/Luckfox-Lyra-Ultra/" rel="alternate" type="text/html" title="Meshtastic with Luckfox Lyra Ultra" /><published>2025-11-30T00:00:00+00:00</published><updated>2026-03-05T20:00:00+00:00</updated><id>/Luckfox-Lyra-Ultra</id><content type="html" xml:base="/Luckfox-Lyra-Ultra/"><![CDATA[<p><img src="/images/posts/lyra/luckfox-lyra-ultra-w.jpg" alt="lyra " /></p>
<h3 id="luckfox-lyra-ultra-w">Luckfox Lyra Ultra W</h3>

<ul>
  <li>RK3506B, triple-core ARM Cortex-A7 and ARM Cortex-M0 Processors, 512MB DDR3</li>
  <li>Full USB port and USB-C port</li>
  <li>Onboard 8GB eMMC</li>
  <li>Onboard WiFi and Onboard bluetooth (optional) - 2.4GHz Wi-Fi6 / Bluetooth 5.2 / BLE</li>
  <li>Onboard 100mbps ethernet with optional POE hat</li>
  <li>Supports MIPI interface (2-lane, 22-pin) with a maximum output resolution of 1280x800@60fps</li>
</ul>

<p>Very nifty board that is very capable and economical. Weight is 0.05 kg (50 grams). Size is roughly 5cm by 5cm by 2cm. It can be powered via USB C or POE. Buy plenty of <a href="https://www.adafruit.com/product/1112">2x13 stacking headers</a>, or even better <a href="https://www.amazon.com/dp/B00KE8K5RG">these</a>.</p>

<p>You can buy <a href="https://www.luckfox.com/Luckfox-Lyra-Ultra?ci=622">Lyra</a> directly from Luckfox. 
You can buy <a href="https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat#how-to-buy">Lyra hats</a> from WeHooper. I recommend the E22P hat.
You can look here for additional boards if desired.</p>

<h3 id="flashing----loader-in-rkdevtool">Flashing -  LOADER in RKDevTool</h3>

<p>Special thanks to vid for all his help!</p>

<p>The only downside to the Lyra Ultra is loading the OS. It’s a pain but manageable. Out of the box, it’s somewhat easy. You can find the <a href="https://wiki.luckfox.com/Luckfox-Lyra/Image-flashing">official instructions here</a>.</p>

<p>Download <a href="https://files.luckfox.com/wiki/Omni3576/TOOLS/DriverAssitant_v5.13.zip">RK Driver Assistant</a> and <a href="https://files.luckfox.com/wiki/Omni3576/TOOLS/RKDevTool_Release_v3.31.zip">RKDevTool</a>. In the config INI file for the RKDevTool, switch language to English before firing up the app.</p>

<p>I recommend <a href="https://github.com/mPWRD-OS">mPWRD-OS</a>, with the current version being <a href="https://github.com/mPWRD-OS/mPWRD-OS/releases/tag/v0.1.0">mPWRD-OS 0.1.0 Alpha</a>. It’s based on Armbian but optimized for meshtastic and theoretically meshcore.</p>

<p>There are two buttons next to the USB C port. The closer one is the BOOT button, the second button further away is the RESET button. Hold down the boot button while plugging in the USB. If you see LOADER, proceed here. If it says NETMASK, skip to resetting the Lyra.</p>

<p><img src="/images/posts/lyra/Flashing.png" alt="Flashing the Lyra" /></p>

<ul>
  <li>Right click and add a new entry. Select STORAGE to be EMMC, ADDRESS should default to 0x00000000, set NAME to system, put full path into PATH field</li>
  <li>Hold the boot button (button closest to USB cable, the one that isn’t labelled “RESET”) while plugging in USB</li>
  <li>RKDEVTool should show “LOADER” mode (NOT “MASKROM” mode)</li>
  <li>In the download tab, uncheck everything.</li>
  <li>Check the box for “write by address” and then press “Run”</li>
</ul>

<p>If successful you’ll see a message like the one on the right. You want to go quickly as the LOADER can timeout. mPWRD-OS should auto-boot and meshtasticd should be reachable with no interaction. I do the initial location via console cable but you can ssh in for that. mpwrd-menu is the command for install and config.</p>

<p>username: root
password: 1234</p>

<h3 id="netmask---resetting-the-lyra-after-a-bad-update">NETMASK - Resetting the Lyra after a bad update</h3>

<p>tl;dr - LOADER is weird proprietary partition. Load Luckfox Ubuntu to get that. THEN burn it a second time with your preferred image. 
Again. LOADER needs a specific boot partition to run. Most OS won’t have that. See below for nerd details.</p>

<p>To get into NETMASK, fire up RKDevTool v3.31. Snag a piece of wire or use a set of tweezers, form it into a U, hit the jumper pads while plugging in the USB button. I clipped the wire at 45 degree angle, but tweezers are better if the points are even. You should see MASKROM at the bottom of the RKDevTool once you’re successful. The pads are slightly recessed so it may take a trial or two. Or five.</p>

<p><img src="/images/posts/lyra/Lyra-reset.png" alt="lyra reset" /></p>

<p>Once it does show up, go to Upgrade Firmware tab, click on Firmware button. Navigate to the unzipped firmware bundle, select update.img, then hit Upgrade.</p>

<p>Give it a bit after the upgrade is successful, then tap the RESET button while holding down the BOOT button. Once LOADER is displayed, you can then load your other OS using the above instructions.</p>

<h3 id="connecting-to-console-via-serial">Connecting to Console via Serial</h3>

<p>Handy if your Lyra doesn’t come up via DHCP. It’s not TECHNICALLY needed but a very good idea to know how to console in.</p>

<p>Using a <a href="https://www.amazon.com/dp/B083HVM7VZ">USB to UART TTL cable</a>, connect the following:</p>

<p>Assuming ethernet port oriented down, left hand header, inner row</p>
<ul>
  <li>GND — MUST be shared with USB-TTL GND (third down)</li>
  <li>TX (board) → connect to RX on USB-TTL (fourth down)</li>
  <li>RX (board) → connect to TX on USB-TTL (fifth down)</li>
</ul>

<p>Refer to this diagram:</p>

<p><img src="/images/posts/lyra/lyra-pinout.jpg" alt="lyra pinout" /></p>

<p>Luckfox RK3506 boards use:</p>

<ul>
  <li>Baud: 1500000 (not a typo)</li>
  <li>Data: 8</li>
  <li>Parity: N</li>
  <li>Stop: 1</li>
  <li>Flow control: None</li>
</ul>

<h3 id="logging-in">Logging in</h3>

<p>Plug in POE hat. Connect to POE switch or injector. Check your router to see what IP it has or run a network scan for port 22</p>

<p>SSH in with putty or connect via serial</p>

<p>If using Luckfox Ubuntu:
Username: lyra
Password: luckfox</p>

<p>If using Armbian:
username: root
password: 1234</p>

<h3 id="setup-for-non-mpwrd-os-linux-os-on-luckfox">Setup for NON-mPWRD-OS linux OS on Luckfox</h3>

<p>Seriously, skip all this and use mPWRD-OS. If you’re hardcore tho, here’s manual setup instructions:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c"># New system updates</span>
<span class="nb">sudo </span>apt update <span class="nt">-y</span>
<span class="nb">sudo </span><span class="nv">DEBIAN_FRONTEND</span><span class="o">=</span>noninteractive <span class="se">\</span>
apt-get <span class="nt">-y</span> <span class="se">\</span>
  <span class="nt">-o</span> Dpkg::Options::<span class="o">=</span><span class="s2">"--force-confdef"</span> <span class="se">\</span>
  <span class="nt">-o</span> Dpkg::Options::<span class="o">=</span><span class="s2">"--force-confold"</span> <span class="se">\</span>
  dist-upgrade

<span class="c"># Meshtastic install</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>software-properties-common <span class="nt">-y</span>
<span class="nb">sudo </span>add-apt-repository ppa:meshtastic/beta
<span class="nb">sudo </span>apt update <span class="nt">-y</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>meshtasticd i2c-tools <span class="nt">-y</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>pipx <span class="nt">-y</span> <span class="o">&amp;&amp;</span> pipx <span class="nb">install</span> <span class="s2">"meshtastic[cli]"</span>
pipx ensurepath


<span class="c"># Enable SPI</span>
<span class="c"># 1 Advanced Options -&gt; 4 SPI -&gt; (enter)  -&gt; 1 Enable</span>
<span class="nb">sudo </span>luckfox-config


<span class="c"># Set SPI pins</span>
<span class="nb">sudo </span>nano /etc/luckfox.cfg
<span class="c"># use pins from below</span>

<span class="c"># Add these uncommented lines to /etc/luckfox.conf</span>
<span class="c"># Settings</span>
<span class="c">#SPI0_STATUS=1</span>
<span class="c">#SPI0_SPEED=20000000</span>
<span class="c">#SPI0_SCLK_RM_IO=8</span>
<span class="c">#SPI0_MISO_RM_IO=7</span>
<span class="c">#SPI0_MOSI_RM_IO=6</span>
<span class="c">#SPI0_CS_RM_IO=10</span>

<span class="c"># reboot</span>

<span class="c"># Turns on auto-discovery</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>avahi-daemon
<span class="nb">sudo tee</span> /etc/avahi/services/meshtastic.service <span class="o">&gt;</span> /dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
&lt;?xml version="1.0" standalone="no"?&gt;&lt;!--*-nxml-*--&gt;
&lt;!DOCTYPE service-group SYSTEM "avahi-service.dtd"&gt;
&lt;service-group&gt;
    &lt;name&gt;Meshtastic&lt;/name&gt;
    &lt;service protocol="ipv4"&gt;
        &lt;type&gt;_meshtastic._tcp&lt;/type&gt;
        &lt;port&gt;4403&lt;/port&gt;
    &lt;/service&gt;
&lt;/service-group&gt;
</span><span class="no">EOF
</span><span class="nb">sudo </span>systemctl <span class="nb">enable </span>avahi-daemon <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>systemctl start avahi-daemon


<span class="c"># Rename the machine</span>

<span class="nb">echo</span> <span class="s2">"KD3BQB-LF-NodeXX"</span> | <span class="nb">sudo tee</span> /etc/hostname
<span class="nb">sudo tee</span> /etc/hosts <span class="o">&gt;</span>/dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
127.0.0.1   localhost
127.0.1.1   KD3BQB-LF-NodeXX
</span><span class="no">EOF

</span><span class="c"># reboot</span>
<span class="c"># sudo reboot</span>

<span class="c"># Get config file</span>
<span class="nb">cd</span> /etc/meshtasticd/config.d/
wget <span class="nt">-O</span> /etc/meshtasticd/config.d/lyra_ultra_hat_1W.yaml https://github.com/wehooper4/Meshtastic-Hardware/raw/refs/heads/main/Luckfox%20Ultra%20Hat/lyra_ultra_hat_1W.yaml
<span class="c"># wget -O /etc/meshtasticd/config.d/lyra_ultra_hat_2W.yaml https://github.com/wehooper4/Meshtastic-Hardware/raw/refs/heads/main/Luckfox%20Ultra%20Hat/lyra_ultra_hat_2W.yaml</span>
<span class="c"># If using E22P, use 1W config file and just set power level to 18. Won't burn itself up, but won't give you more dbm</span>
<span class="c"># If you have problems below such as "No sx1262 radio", try uncommenting the CS line</span>

<span class="c"># uncomment eth0 or set your MACAddressSource</span>
<span class="nb">sudo </span>nano /etc/meshtasticd/config.yaml
<span class="c"># Fire up meshtasticd </span>
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>meshtasticd <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>systemctl start meshtasticd

<span class="c"># Meshtastic CLI</span>
meshtastic <span class="nt">--host</span> <span class="nt">--set</span> lora.region <span class="s2">"US"</span>
meshtastic <span class="nt">--host</span> <span class="nt">--set-owner</span> <span class="s2">"SUSQ VAL PA Mesh - Town - Tower"</span>
meshtastic <span class="nt">--host</span> <span class="nt">--set-owner-short</span> <span class="s2">"SVMI"</span>
meshtastic <span class="nt">--host</span>  <span class="nt">--export-config</span> | <span class="nb">grep</span> <span class="s2">"Key:"</span>

</code></pre></div></div>

<p>Edit config.yaml to set MAC (use MACAddressSource eth0) and node limits (100-200). 
Troubleshoot with <code class="language-plaintext highlighter-rouge">journalctl -xeu meshtasticd.service</code> or run <code class="language-plaintext highlighter-rouge">meshtasticd</code> manually. 
Configure through the Meshtastic app. Use the Network option on the Cloud tab in the app.</p>

<h3 id="wifi">WiFi</h3>

<p>Lyra Ultra W has WiFi built in and mPWRD-OS should have it running. Check by running ‘ip link show’ and making sure wlan0 is there</p>

<p>Adding WiFi network manually</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmcli connection add <span class="nb">type </span>wifi ifname wlan0 con-name mywifi ssid <span class="s2">"SSID_NAME"</span>
<span class="nb">sudo </span>nmcli connection modify mywifi wifi-sec.key-mgmt wpa-psk
<span class="nb">sudo </span>nmcli connection modify mywifi wifi-sec.psk <span class="s2">"YOUR_PASSWORD"</span>
<span class="nb">sudo </span>nmcli connection modify mywifi connection.autoconnect <span class="nb">yes</span>
</code></pre></div></div>

<p>Setting up AP mode</p>

<p>Save to setup-ap.sh and run sudo bash ./setup-ap.sh</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># Simple AP setup script for Debian</span>
<span class="c"># Run as root</span>

<span class="c">### CONFIGURATION ###</span>
<span class="nv">AP_SSID</span><span class="o">=</span><span class="s2">"MyAccessPoint"</span>
<span class="nv">AP_PASSWORD</span><span class="o">=</span><span class="s2">"SuperSecret123"</span>
<span class="nv">WLAN_IF</span><span class="o">=</span><span class="s2">"wlan0"</span>
<span class="nv">AP_IP</span><span class="o">=</span><span class="s2">"192.168.50.1"</span>
<span class="nv">DHCP_RANGE_START</span><span class="o">=</span><span class="s2">"192.168.50.10"</span>
<span class="nv">DHCP_RANGE_END</span><span class="o">=</span><span class="s2">"192.168.50.200"</span>
<span class="nv">UPLINK_IF</span><span class="o">=</span><span class="s2">"eth0"</span>   <span class="c"># or wwan0 if you want to NAT to LTE</span>
<span class="c">####################</span>

<span class="nb">set</span> <span class="nt">-e</span>

<span class="nb">echo</span> <span class="s2">"[*] Installing required packages..."</span>
<span class="nb">sudo </span>apt <span class="nt">-o</span> Acquire::ForceIPv4<span class="o">=</span><span class="nb">true </span>update
<span class="nb">sudo </span>apt <span class="nt">-o</span> Acquire::ForceIPv4<span class="o">=</span><span class="nb">true install</span> <span class="nt">-y</span> hostapd dnsmasq iptables-persistent ifupdown

<span class="nb">echo</span> <span class="s2">"[*] Stopping services until configured..."</span>
systemctl stop hostapd <span class="o">||</span> <span class="nb">true
</span>systemctl stop dnsmasq <span class="o">||</span> <span class="nb">true

echo</span> <span class="s2">"[*] Configuring static IP on </span><span class="nv">$WLAN_IF</span><span class="s2">..."</span>
<span class="nb">cat</span> <span class="o">&gt;</span>/etc/network/interfaces.d/<span class="nv">$WLAN_IF</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
auto </span><span class="nv">$WLAN_IF</span><span class="sh">
iface </span><span class="nv">$WLAN_IF</span><span class="sh"> inet static
    address </span><span class="nv">$AP_IP</span><span class="sh">
    netmask 255.255.255.0
</span><span class="no">EOF
</span>ifdown <span class="nv">$WLAN_IF</span> <span class="o">||</span> <span class="nb">true
</span>ifup <span class="nv">$WLAN_IF</span>

<span class="nb">echo</span> <span class="s2">"[*] Configuring dnsmasq..."</span>
<span class="nb">mv</span> /etc/dnsmasq.conf /etc/dnsmasq.conf.orig.<span class="si">$(</span><span class="nb">date</span> +%s<span class="si">)</span> <span class="o">||</span> <span class="nb">true
cat</span> <span class="o">&gt;</span>/etc/dnsmasq.conf <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
interface=</span><span class="nv">$WLAN_IF</span><span class="sh">
dhcp-range=</span><span class="nv">$DHCP_RANGE_START</span><span class="sh">,</span><span class="nv">$DHCP_RANGE_END</span><span class="sh">,255.255.255.0,24h
</span><span class="no">EOF

</span><span class="nb">echo</span> <span class="s2">"[*] Configuring hostapd..."</span>
<span class="nb">cat</span> <span class="o">&gt;</span>/etc/hostapd/hostapd.conf <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
interface=</span><span class="nv">$WLAN_IF</span><span class="sh">
driver=nl80211
ssid=</span><span class="nv">$AP_SSID</span><span class="sh">
hw_mode=g
channel=6
wmm_enabled=1
auth_algs=1
ignore_broadcast_ssid=0
wpa=2
wpa_passphrase=</span><span class="nv">$AP_PASSWORD</span><span class="sh">
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
</span><span class="no">EOF

</span><span class="c"># Point hostapd to config</span>
<span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s|^#DAEMON_CONF=.*|DAEMON_CONF=</span><span class="se">\"</span><span class="s2">/etc/hostapd/hostapd.conf</span><span class="se">\"</span><span class="s2">|"</span> /etc/default/hostapd

<span class="nb">echo</span> <span class="s2">"[*] Enabling IP forwarding..."</span>
<span class="nb">sed</span> <span class="nt">-i</span> <span class="s1">'s/^#\?net.ipv4.ip_forward.*/net.ipv4.ip_forward=1/'</span> /etc/sysctl.conf
sysctl <span class="nt">-p</span>

<span class="nb">echo</span> <span class="s2">"[*] Setting up NAT to </span><span class="nv">$UPLINK_IF</span><span class="s2">..."</span>
iptables <span class="nt">-t</span> nat <span class="nt">-A</span> POSTROUTING <span class="nt">-o</span> <span class="nv">$UPLINK_IF</span> <span class="nt">-j</span> MASQUERADE
iptables <span class="nt">-A</span> FORWARD <span class="nt">-i</span> <span class="nv">$UPLINK_IF</span> <span class="nt">-o</span> <span class="nv">$WLAN_IF</span> <span class="nt">-m</span> state <span class="nt">--state</span> RELATED,ESTABLISHED <span class="nt">-j</span> ACCEPT
iptables <span class="nt">-A</span> FORWARD <span class="nt">-i</span> <span class="nv">$WLAN_IF</span> <span class="nt">-o</span> <span class="nv">$UPLINK_IF</span> <span class="nt">-j</span> ACCEPT
netfilter-persistent save

<span class="nb">echo</span> <span class="s2">"[*] Enabling services..."</span>
systemctl unmask hostapd
systemctl <span class="nb">enable </span>hostapd
systemctl <span class="nb">enable </span>dnsmasq
systemctl restart hostapd
systemctl restart dnsmasq

<span class="nb">echo</span> <span class="s2">"[+] Access Point setup complete!"</span>
<span class="nb">echo</span> <span class="s2">"    SSID: </span><span class="nv">$AP_SSID</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"    Password: </span><span class="nv">$AP_PASSWORD</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"    Interface: </span><span class="nv">$WLAN_IF</span><span class="s2"> (</span><span class="nv">$AP_IP</span><span class="s2">)"</span>
</code></pre></div></div>

<h3 id="reliability">Reliability</h3>

<p>To auto-restart Meshtastic and reboot weekly:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo tee</span> /usr/local/bin/check_meshtasticd.sh <span class="o">&gt;</span> /dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
#!/bin/bash
SERVICE="meshtasticd"
if ! pgrep -x "</span><span class="nv">$SERVICE</span><span class="sh">" &gt; /dev/null; then
    echo "</span><span class="si">$(</span><span class="nb">date</span><span class="si">)</span><span class="sh">: </span><span class="nv">$SERVICE</span><span class="sh"> not running, restarting..." &gt;&gt; /var/log/meshtasticd_monitor.log
    systemctl restart </span><span class="nv">$SERVICE</span><span class="sh">
fi
</span><span class="no">EOF
</span><span class="nb">sudo chmod</span> +x /usr/local/bin/check_meshtasticd.sh
<span class="nb">sudo </span>crontab <span class="nt">-e</span>
<span class="c"># 0 * * * * /usr/local/bin/check_meshtasticd.sh</span>
<span class="c"># 0 1 * * 1 /sbin/reboot</span>
</code></pre></div></div>

<p>If you’re not very experienced with Linux, remember to log in every so often to run updates. You can automate that as well.</p>

<h3 id="remote-access">Remote Access</h3>

<p>If you use Mark’s Ubuntu image, it may not have TUN service built into the kernel and it won’t work. 
If you use Luckfox community Ubuntu image, not sure yet. Will test it.
If you use Armbian/mPWRD-OS, it should.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>curl
curl <span class="nt">-fsSL</span> https://tailscale.com/install.sh | sh
<span class="nb">sudo </span>tailscale up
</code></pre></div></div>

<p>Copy URL to web browser.</p>

<p>Once you have tailscale installed, you can also install tailscale on your phone and use the Meshtastic app to connect to your node from anywhere in the world.</p>

<h3 id="power-consumption">Power consumption</h3>

<p><img src="/images/posts/lyra/lyra-USB-power.jpg" alt="lyra power" /></p>

<p>With no hat:</p>

<ul>
  <li>USB - 1W, 0.2A, 5V</li>
  <li>POE - 1.5W, 27 mA. Switch is pushing at 54-56V.</li>
</ul>

<h3 id="backboards-for-lyra">Backboards for Lyra</h3>

<p>TODO STLs and laser files</p>

<h3 id="loader-partition-details">LOADER Partition Details</h3>

<h3 id="tower-hardware">Tower Hardware</h3>

<p>Nebra case can be purchased for $15 or less. Shipping is the expensive part. In smaller quantities, it makes sense to buy same case from Aliexpress.</p>

<p>For smaller builds, I’m experimenting with smaller cases.</p>

<p>Nebra Strut, reach out to Mark for a quote. Should be $40-50 ish if buying a bunch</p>

<p>13 or 23 inch strut
Angle iron on both ends For the arm:
one arm, centered on both sides
centering on enclosure side keeps from interferring with antennas on either side of enclosure.
Centering on tower side is just for ease of shipping/storage. If you think it’s an issue, can go towards one side On the enclosure side:
flat side out
9 inches long angle iron (just slightly shorter than the case)
two mounting holes, centered width wise, 5/16 holes, 7.5 inches apart (190mm preferably) On tower side:
9 inches long angle iron
two 1.5 inch U-bolts (change this depending on your tower)
We’re also looking as gusset brackets for longer lifespan. It’s meant to be cheap but strong enough. Not interfere with antennas, just long enough to minimize tower shadow and interference, etc. You’ll need two M6 bolts and washets. I recommend metal banding the case to the strut, mostly to minimize amount of force on the bolts.</p>]]></content><author><name>Casper</name></author><category term="meshtastic" /><category term="LoRA" /><summary type="html"><![CDATA[Gen 3 Tower Nodes]]></summary></entry><entry><title type="html">Install Swagger UI on IIS for Infor IQM / Mongoose</title><link href="/Install-SwaggerUI-for-IQM-on-IIS/" rel="alternate" type="text/html" title="Install Swagger UI on IIS for Infor IQM / Mongoose" /><published>2025-09-29T00:00:00+00:00</published><updated>2025-09-29T00:00:00+00:00</updated><id>/Install-SwaggerUI-for-IQM-on-IIS</id><content type="html" xml:base="/Install-SwaggerUI-for-IQM-on-IIS/"><![CDATA[<p><img src="/images/posts/iqm/swaggerui.png" alt="Swagger YU" /></p>

<h2 id="intro">Intro</h2>

<p><a href="https://swagger.io/tools/swagger-ui/">Swagger UI</a> is a graphical interface for swagger rather than just a JSON file. You can also run demo API requests directly from the browser. Lets you prototype much faster and easier.</p>

<p>It just static HTML and Javascript, so nearly zero overhead. Has no impact on your production system.</p>

<p>This guide:</p>
<ul>
  <li><strong>Does not</strong> change the IQM/Mongoose binaries, app pool, or web.config</li>
  <li>Adds a <strong>virtual directory</strong> (or app) that serves only static content</li>
  <li>Points Swagger UI at your Mongoose OpenAPI endpoints (e.g., <code class="language-plaintext highlighter-rouge">/IDORequestService/ido/api-docs/</code>)</li>
</ul>

<hr />

<h2 id="prerequisites">Prerequisites</h2>

<ul>
  <li>See <a href="https://casper.im/tags/#iqm">last IQM posts</a> on tokens, setup, finding IDO names, etc.</li>
  <li>Windows Server with <strong>IIS</strong> and <strong>Static Content</strong> feature enabled</li>
  <li>An IQM/Mongoose instance reachable at a URL like:
    <ul>
      <li>Generic docs index: <code class="language-plaintext highlighter-rouge">http://&lt;host&gt;/IDORequestService/ido/api-docs/</code></li>
      <li>REST v2 (if generated via REST API Wizard):<br />
<code class="language-plaintext highlighter-rouge">http://&lt;host&gt;/IDORequestService/ido/api-docs/v2/</code> (index)<br />
<code class="language-plaintext highlighter-rouge">http://&lt;host&gt;/IDORequestService/ido/api-docs/v2/&lt;IDOName&gt;</code> (per-IDO)</li>
    </ul>
  </li>
  <li>RDP access and admin rights to the IQM IIS server</li>
</ul>

<blockquote>
  <p><strong>Tip</strong><br />
If <code class="language-plaintext highlighter-rouge">/api-docs/v2/</code> returns 404 but <code class="language-plaintext highlighter-rouge">/api-docs/</code> works, run the <strong>REST API Wizard</strong> in Mongoose to generate the typed REST v2 endpoints. Once generated, their Swagger becomes available under <code class="language-plaintext highlighter-rouge">/api-docs/v2/</code>.</p>
</blockquote>

<hr />

<h2 id="quick-test-optional">Quick Test (optional)</h2>

<p>From the IQM server (PowerShell):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$urls</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
  </span><span class="s2">"http://&lt;host&gt;/IDORequestService/ido/api-docs/"</span><span class="p">,</span><span class="w">
  </span><span class="s2">"http://&lt;host&gt;/IDORequestService/ido/api-docs/v2/"</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nv">$urls</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"{0} -&gt; {1}"</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="bp">$_</span><span class="p">,(</span><span class="n">Invoke-WebRequest</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="nt">-UseBasicParsing</span><span class="w"> </span><span class="nt">-TimeoutSec</span><span class="w"> </span><span class="nx">5</span><span class="p">)</span><span class="o">.</span><span class="nf">StatusCode</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"{0} -&gt; FAILED: {1}"</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="bp">$_</span><span class="p">,</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="step-1--download-swagger-ui">Step 1 — Download Swagger UI</h2>

<ol>
  <li>Grab the latest <a href="https://github.com/swagger-api/swagger-ui/releases"><strong>Swagger UI</strong></a> release ZIP from the official repo.</li>
  <li>Extract the contents of the <strong><code class="language-plaintext highlighter-rouge">dist</code></strong> folder to a <strong>new</strong> path on the server, for example:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\inetpub\wwwroot\swaggerui
</code></pre></div></div>

<p>Your folder should contain files like <code class="language-plaintext highlighter-rouge">index.html</code>, <code class="language-plaintext highlighter-rouge">swagger-initializer.js</code>, <code class="language-plaintext highlighter-rouge">swagger-ui.css</code>, etc.</p>

<p>Do NOT put in your IQM folder.</p>

<hr />

<h2 id="step-2--point-swagger-ui-at-mongoose-api-docs">Step 2 — Point Swagger UI at Mongoose API Docs</h2>

<p>Edit <code class="language-plaintext highlighter-rouge">C:\inetpub\wwwroot\swaggerui\swagger-initializer.js</code>.<br />
You have two options:</p>

<h3 id="option-a--single-spec-url">Option A — Single spec URL</h3>

<p>Do this first. Try Option B later unless you know what you’re doing.</p>

<p>Replace the default config with:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">window</span><span class="p">.</span><span class="nx">ui</span> <span class="o">=</span> <span class="nx">SwaggerUIBundle</span><span class="p">({</span>
  <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://&lt;host&gt;/IDORequestService/ido/api-docs/</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">dom_id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#swagger-ui</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">deepLinking</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
  <span class="na">presets</span><span class="p">:</span> <span class="p">[</span>
    <span class="nx">SwaggerUIBundle</span><span class="p">.</span><span class="nx">presets</span><span class="p">.</span><span class="nx">apis</span><span class="p">,</span>
    <span class="nx">SwaggerUIStandalonePreset</span>
  <span class="p">],</span>
  <span class="na">layout</span><span class="p">:</span> <span class="dl">"</span><span class="s2">StandaloneLayout</span><span class="dl">"</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="option-b--multiple-specs-dropdown">Option B — Multiple specs (dropdown)</h3>
<p>Let users switch between the generic index and specific REST v2 IDOs:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">window</span><span class="p">.</span><span class="nx">ui</span> <span class="o">=</span> <span class="nx">SwaggerUIBundle</span><span class="p">({</span>
  <span class="na">urls</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://&lt;host&gt;/IDORequestService/ido/api-docs/</span><span class="dl">"</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Mongoose API Docs (Index)</span><span class="dl">"</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://&lt;host&gt;/IDORequestService/ido/api-docs/v2/VQUnits</span><span class="dl">"</span><span class="p">,</span>  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">VQUnits (REST v2)</span><span class="dl">"</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://&lt;host&gt;/IDORequestService/ido/api-docs/v2/VQSpecs</span><span class="dl">"</span><span class="p">,</span>  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">VQSpecs (REST v2)</span><span class="dl">"</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://&lt;host&gt;/IDORequestService/ido/api-docs/v2/VQInspections</span><span class="dl">"</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">VQInspections (REST v2)</span><span class="dl">"</span> <span class="p">}</span>
  <span class="p">],</span>
  <span class="na">dom_id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#swagger-ui</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">deepLinking</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
  <span class="na">presets</span><span class="p">:</span> <span class="p">[</span>
    <span class="nx">SwaggerUIBundle</span><span class="p">.</span><span class="nx">presets</span><span class="p">.</span><span class="nx">apis</span><span class="p">,</span>
    <span class="nx">SwaggerUIStandalonePreset</span>
  <span class="p">],</span>
  <span class="na">layout</span><span class="p">:</span> <span class="dl">"</span><span class="s2">StandaloneLayout</span><span class="dl">"</span>
<span class="p">});</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Tip</strong><br />
If you don’t know the exact IDO names, start with the index (<code class="language-plaintext highlighter-rouge">/api-docs/</code> or <code class="language-plaintext highlighter-rouge">/api-docs/v2/</code>) and follow links from there.</p>
</blockquote>

<hr />

<h2 id="step-3--create-the-iis-virtual-directory">Step 3 — Create the IIS Virtual Directory</h2>

<ol>
  <li>Open <strong>IIS Manager</strong>.</li>
  <li>In <strong>Connections</strong>, expand <strong>Sites</strong> → select the site that hosts IQM.</li>
  <li>Right-click the site → <strong>Add Virtual Directory…</strong>
    <ul>
      <li><strong>Alias:</strong> <code class="language-plaintext highlighter-rouge">swaggerui</code></li>
      <li><strong>Physical path:</strong> <code class="language-plaintext highlighter-rouge">C:\inetpub\wwwroot\swaggerui</code></li>
    </ul>
  </li>
  <li>Click <strong>OK</strong>.</li>
</ol>

<h3 id="convert-to-application">Convert to Application</h3>
<ol>
  <li>Select the new <code class="language-plaintext highlighter-rouge">swaggerui</code> item.</li>
  <li>Right-click → <strong>Convert to Application…</strong></li>
  <li>Use the <strong>same app pool</strong> as the parent site (static files only), or create a <strong>new app pool</strong> for isolation.</li>
  <li>Click <strong>OK</strong>.</li>
</ol>

<h3 id="ensure-static-content-is-enabled">Ensure Static Content is enabled</h3>
<p>On the <strong>server</strong> node → <strong>Modules/Features</strong> → verify <strong>Static Content</strong> is installed.<br />
<em>No custom handlers are required</em>—Swagger UI is HTML/JS/CSS.</p>

<hr />

<h2 id="step-4--test">Step 4 — Test</h2>

<p>Browse to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://&lt;host&gt;/swaggerui/
</code></pre></div></div>

<p>You should see Swagger UI with either the single spec or the dropdown (depending on your initializer).</p>

<ul>
  <li>If the UI loads but the spec fails to load: verify the URLs in <code class="language-plaintext highlighter-rouge">swagger-initializer.js</code>.</li>
  <li>If you see MIME errors: ensure <strong>Static Content</strong> is installed and no restrictive <code class="language-plaintext highlighter-rouge">web.config</code> is blocking JS/CSS.</li>
</ul>

<hr />

<h2 id="security-notes">Security Notes</h2>

<p>tl;dr - 
DO NOT INSTALL THIS ON A PUBLIC WEB SERVER!
You can add authentication for local environment, and probably should.</p>

<ul>
  <li><strong>LAN only vs External:</strong> If you’ll expose Swagger UI outside your LAN, front Mongoose endpoints with <strong>ION API Gateway</strong> (OAuth2, policies, rate limits) rather than publishing raw <code class="language-plaintext highlighter-rouge">/IDORequestService/ido/*</code> URLs.</li>
  <li><strong>Auth context:</strong> Swagger UI will inherit whatever authentication your Mongoose endpoints require. If your IQM site uses Windows Auth/SSO, tests from the server may work differently than remote clients.</li>
  <li><strong>CORS:</strong> Hosting Swagger UI on the <strong>same host</strong> avoids CORS. If you host the UI on a different domain, you may need a reverse proxy or CORS adjustments. CORS is cross-site scripting, bad guys use it and it can be a pain to setup.</li>
</ul>

<hr />

<h2 id="rollback-safe-and-easy">Rollback (safe and easy)</h2>

<p>This setup is non-intrusive. To roll back:</p>
<ul>
  <li>Delete the IIS <strong>application/virtual directory</strong> for <code class="language-plaintext highlighter-rouge">swaggerui</code>.</li>
  <li>Optionally delete <code class="language-plaintext highlighter-rouge">C:\inetpub\wwwroot\swaggerui</code>.</li>
</ul>

<p><em>No changes were made to the IQM/Mongoose app or its app pool.</em></p>

<hr />

<h2 id="no-https">No HTTPS?</h2>

<p>You really should enable HTTPS. But for testing or very small environments, edit swagger-initializer.js with following code example. Replace host with your server name.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">window</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">//&lt;editor-fold desc="Changeable Configuration Block"&gt;</span>

  <span class="c1">// the following lines will be replaced by docker/configurator, when it runs in a docker-container</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">ui</span> <span class="o">=</span> <span class="nx">SwaggerUIBundle</span><span class="p">({</span>
  <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://host/IDORequestService/ido/api-docs/</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">dom_id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#swagger-ui</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">deepLinking</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
  <span class="na">presets</span><span class="p">:</span> <span class="p">[</span> <span class="nx">SwaggerUIBundle</span><span class="p">.</span><span class="nx">presets</span><span class="p">.</span><span class="nx">apis</span><span class="p">,</span> <span class="nx">SwaggerUIStandalonePreset</span> <span class="p">],</span>
  <span class="na">layout</span><span class="p">:</span> <span class="dl">"</span><span class="s2">StandaloneLayout</span><span class="dl">"</span><span class="p">,</span>
  <span class="c1">// Override servers after load</span>
  <span class="na">onComplete</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">spec</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">ui</span><span class="p">.</span><span class="nx">specSelectors</span><span class="p">.</span><span class="nx">specJson</span><span class="p">().</span><span class="nx">toJS</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">spec</span><span class="p">.</span><span class="nx">openapi</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">spec</span><span class="p">.</span><span class="nx">servers</span> <span class="o">=</span> <span class="p">[</span>
        <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://host/IDORequestService/ido</span><span class="dl">"</span> <span class="p">},</span>
        <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://host/IDORequestService/ido</span><span class="dl">"</span> <span class="p">}</span>
      <span class="p">];</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">spec</span><span class="p">.</span><span class="nx">schemes</span> <span class="o">=</span> <span class="p">[</span><span class="dl">"</span><span class="s2">http</span><span class="dl">"</span><span class="p">,</span><span class="dl">"</span><span class="s2">https</span><span class="dl">"</span><span class="p">];</span>
    <span class="p">}</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">ui</span><span class="p">.</span><span class="nx">specActions</span><span class="p">.</span><span class="nx">updateJsonSpec</span><span class="p">(</span><span class="nx">spec</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">});</span>


  <span class="c1">//&lt;/editor-fold&gt;</span>
<span class="p">};</span>
</code></pre></div></div>

<hr />

<h2 id="optional-scripted-setup-powershell">Optional: Scripted Setup (PowerShell)</h2>

<blockquote>
  <p>Adjust the version and paths to taste.
Be very cautious as I didn’t thoroughly test this. I only needed to run once. Might nuke this section</p>
</blockquote>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"5.29.1"</span><span class="w"> </span><span class="c"># version at time of writing this</span><span class="w">
</span><span class="nv">$zipUrl</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://github.com/swagger-api/swagger-ui/archive/refs/tags/v</span><span class="nv">$version</span><span class="s2">.zip"</span><span class="w">
</span><span class="nv">$tmpZip</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">TEMP</span><span class="s2">\swaggerui-</span><span class="nv">$version</span><span class="s2">.zip"</span><span class="w">
</span><span class="nv">$dest</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s2">"C:\inetpub\wwwroot\swaggerui"</span><span class="w">

</span><span class="n">Invoke-WebRequest</span><span class="w"> </span><span class="nv">$zipUrl</span><span class="w"> </span><span class="nt">-OutFile</span><span class="w"> </span><span class="nv">$tmpZip</span><span class="w">
</span><span class="n">Expand-Archive</span><span class="w"> </span><span class="nv">$tmpZip</span><span class="w"> </span><span class="nt">-DestinationPath</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">TEMP</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="nv">$distPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Join-Path</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">TEMP</span><span class="w"> </span><span class="s2">"swagger-ui-</span><span class="nv">$version</span><span class="s2">\dist"</span><span class="w">

</span><span class="n">New-Item</span><span class="w"> </span><span class="nt">-ItemType</span><span class="w"> </span><span class="nx">Directory</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$dest</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-Null</span><span class="w">
</span><span class="nx">Copy-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="p">(</span><span class="n">Join-Path</span><span class="w"> </span><span class="nv">$distPath</span><span class="w"> </span><span class="s2">"*"</span><span class="p">)</span><span class="w"> </span><span class="nt">-Destination</span><span class="w"> </span><span class="nv">$dest</span><span class="w"> </span><span class="nt">-Recurse</span><span class="w"> </span><span class="nt">-Force</span><span class="w">

</span><span class="c"># Write a minimal initializer that points at the generic Mongoose index</span><span class="w">
</span><span class="nv">$init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@"
window.ui = SwaggerUIBundle({
  url: "http://&lt;host&gt;/IDORequestService/ido/api-docs/",
  dom_id: '#swagger-ui',
  deepLinking: true,
  presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ],
  layout: "StandaloneLayout"
});
"@</span><span class="w">
</span><span class="n">Set-Content</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="p">(</span><span class="n">Join-Path</span><span class="w"> </span><span class="nv">$dest</span><span class="w"> </span><span class="s2">"swagger-initializer.js"</span><span class="p">)</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nv">$init</span><span class="w"> </span><span class="nt">-Encoding</span><span class="w"> </span><span class="n">UTF8</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Swagger UI files staged in </span><span class="nv">$dest</span><span class="s2">. Create an IIS virtual directory named 'swaggerui' pointing to this folder."</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="frequently-asked">Frequently Asked</h2>

<p><strong>Q: <code class="language-plaintext highlighter-rouge">/api-docs/</code> works, but <code class="language-plaintext highlighter-rouge">/api-docs/v2/</code> doesn’t.</strong><br />
A: Generate the REST v2 endpoints with the <strong>REST API Wizard</strong> in Mongoose (choose the IDOs you need). That process publishes the per-IDO Swagger under <code class="language-plaintext highlighter-rouge">/api-docs/v2/…</code>.</p>

<p><strong>Q: Can I list multiple IQM specs in one UI?</strong><br />
A: Yes—use the <code class="language-plaintext highlighter-rouge">urls: []</code> array in <code class="language-plaintext highlighter-rouge">swagger-initializer.js</code> (see <strong>Option B</strong> above).</p>

<p><strong>Q: Will this break IQM updates?</strong><br />
A: Nope, Swagger UI lives in a separate folder/app. Removing it is just deleting the app/virtual directory in IIS manager and on disk</p>

<hr />

<h2 id="see-also">See also</h2>

<ul>
  <li>Your IQM/Mongoose API docs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">http://&lt;host&gt;/IDORequestService/ido/api-docs/</code></li>
      <li><code class="language-plaintext highlighter-rouge">http://&lt;host&gt;/IDORequestService/ido/api-docs/v2/</code></li>
    </ul>
  </li>
  <li>Related: <a href="https://casper.im/tags/#iqm"><em>“Infor IQM API – Part 2”</em></a> style walkthroughs on crafting requests, using REST v2, and publishing. I link to the Infor resources there.</li>
</ul>

<hr />]]></content><author><name>Casper</name></author><category term="Infor" /><category term="IQM" /><category term="Mongoose" /><category term="Swagger" /><category term="IIS" /><category term="API" /><summary type="html"><![CDATA[Safe, non-intrusive way to host Swagger UI on your IQM server and point it at Mongoose API docs (including REST v2) without touching the existing app.]]></summary></entry><entry><title type="html">Nebra Cell Modem Setup and Config</title><link href="/Nebra-Cell-Modem/" rel="alternate" type="text/html" title="Nebra Cell Modem Setup and Config" /><published>2025-08-23T00:00:00+00:00</published><updated>2025-08-23T20:00:00+00:00</updated><id>/Nebra-Cell-Modem</id><content type="html" xml:base="/Nebra-Cell-Modem/"><![CDATA[<p><img src="/images/posts/nebra/nebra-case.jpg" alt="Meshtastic Pi" /></p>

<h3 id="nebra-cell-modem">Nebra Cell Modem</h3>

<p>After picking up a couple Nebra units, I read the manuals and specs sheets. And noticed the official optional cell modem. The original MSRP was insanely high but now is on eBay for $20. The model is the Quectel EG25-G Mini PCIe 4G Mobile Broadband Card w/ Antennas.</p>

<p>The kit comes with:</p>
<ul>
  <li>Quectel EG25-G Mini PCIe card</li>
  <li>2x LTE antenna</li>
  <li>2x ipex to N bulkhead</li>
</ul>

<p>CAD model is available <a href="https://www.digikey.com/en/models/13278349">here</a>.</p>

<p>Bands are:</p>
<ul>
  <li>LTE-FDD	B1, B2, B3, B4, B5, B7, B8, B12, B13, B18, B19, B20, B25, B26, B28</li>
  <li>LTE-TDD	B38, B39, B40, B41</li>
  <li>WCDMA	B1, B2, B4, B5, B6, B8, B19</li>
  <li>GSM	850, 900, 1800, 1900 MHz</li>
</ul>

<p>Uses 3x IPEX-1 connectors : LTE main antenna + LTE diversity antenna + GNSS antenna)</p>

<p>It can do LTE, UMTS/HSPA+, and GSM/GPRS/EDGE. It can do MIMO for 150Mbps down and 50Mbps up. It uses multi-constellation Qualcomm IZat GNSS Gen8C Lite that supports GPS, GLONASS, BDS, Galileo and QZSS. SMS supports MT, MO, CB, Text, PDU. It can do voice calls, which has interesting possibilities.</p>

<h3 id="hardware-install">Hardware Install</h3>

<ul>
  <li>Insert SIM card - you’ll need it even just for GPS to work reliably. I don’t think it even needs to have service</li>
  <li>Remove metal clip closer to the edge of the system board</li>
  <li>Slide in the Mini PCIe at 30-45 degrees into the connector side</li>
  <li>Press down to flat, there are two spring loaded clips that will hold it in position</li>
  <li>Connect MAIN and GNSS ipex bulkheads and antennas. Leave DIV empty.</li>
</ul>

<p>I recommend MAIN be on the opposite side of the enclosure from your 915MHz antenna for meshtastic. GNSS is passive so won’t interfere, I put that besides the main meshtastic antenna.</p>

<p>GPS works absolutely fine off the included LTE antenna. I’m connecting to 10-12 GPS sats indoors. If you need hyper accurate time, get an active GPS antenna.</p>

<h3 id="setup">Setup</h3>

<p>This is brand new and I’m still working through the config. Please reach out if you run into any issues.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c"># install nmcli tools and everything else that'll be needed</span>
<span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install </span>network-manager modemmanager gpsd gpsd-clients chrony socat <span class="nt">-y</span>

<span class="nb">sudo </span>systemctl <span class="nb">enable </span>NetworkManager
<span class="nb">sudo </span>systemctl start NetworkManager

<span class="nb">sudo </span>systemctl <span class="nb">enable </span>ModemManager
<span class="nb">sudo </span>systemctl start ModemManager

<span class="c"># list devices</span>
nmcli device

<span class="c"># list status</span>
mmcli <span class="nt">-L</span>
<span class="c"># look for /org/freedesktop/ModemManager1/Modem/0 [QUECTEL INCORPORATED] EG25</span>

<span class="c"># If modem is 0 use -m 0 for everything, if 1 change to -m 1</span>
<span class="nb">sudo </span>mmcli <span class="nt">-m</span> 0 <span class="nt">--enable</span>

<span class="c"># Activate cell modem without data</span>
<span class="nb">sudo </span>nmcli connection add <span class="nb">type </span>gsm ifname <span class="s2">"*"</span> con-name mycell
<span class="c"># If you actually want to use the data, use:</span>
<span class="c"># sudo nmcli connection add type gsm ifname "*" con-name mycell gsm.apn &lt;your_carrier_apn&gt;</span>
<span class="nb">sudo </span>nmcli connection up mycell
<span class="nb">sudo </span>nmcli connection modify mycell connection.autoconnect <span class="nb">yes</span>


<span class="c"># run to ID ports</span>
mmcli <span class="nt">-m</span> 0 
<span class="c"># output should be: ports: cdc-wdm0 (qmi), ttyUSB0 (qcdm), ttyUSB1 (gps), ttyUSB2 (at), ttyUSB3 (at), wwan0 (net)</span>
<span class="c"># Note which one is (gps)</span>

<span class="c"># Enable GPS</span>
<span class="c">#</span>
<span class="c"># DO NOT USE ANY OTHER GPS MODES! </span>
<span class="c"># gps-unmanaged lets you use gpsd, enabling any other gps modes (nmea) makes modemmanager take control of the GPS feed</span>
<span class="c">#</span>
<span class="nb">sudo </span>mmcli <span class="nt">-m</span> 0 <span class="nt">--location-enable-gps-unmanaged</span>
<span class="nb">sudo </span>mmcli <span class="nt">-m</span> 0 <span class="nt">--location-set-enable-signal</span>
<span class="nb">sudo </span>mmcli <span class="nt">-m</span> 0 <span class="nt">--location-get</span>
mmcli <span class="nt">-m</span> 0 <span class="nt">--location-status</span>


<span class="c"># sudo nano /etc/default/gpsd</span>
<span class="c">#</span>
<span class="c"># change /dev/ttyUSB1 to whatever port above uses (gps)</span>
<span class="nb">sudo tee</span> /etc/default/gpsd <span class="o">&gt;</span> /dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
START_DAEMON="true"
GPSD_OPTIONS="-n"
DEVICES="/dev/ttyUSB1"
USBAUTO="false"
# Socket-activated service will start gpsd when a client connects.
</span><span class="no">EOF

</span><span class="c"># Turn on gpsd</span>
<span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> gpsd
<span class="nb">sudo </span>systemctl start gpsd


<span class="c">#</span>
<span class="c"># Connecting gpsd to meshtastic</span>
<span class="c">#</span>
<span class="c"># Meshtastic only seems to support SerialPath. I went with PTY rather than FIFO so that it can pass traffic via TTY.</span>

<span class="c"># PTY creator</span>
<span class="nb">sudo tee</span> /etc/systemd/system/meshtastic-pty.service <span class="o">&gt;</span> /dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
[Unit]
Description=Meshtastic virtual PTY pair for GPS (socat)
After=network.target

[Service]
Type=simple
RuntimeDirectory=meshtastic
# Create two PTYs with stable links and permissive mode (tighten later if desired)
ExecStart=/usr/bin/env socat -d -d pty,raw,echo=0,link=/run/meshtastic/gps_in,mode=666 pty,raw,echo=0,link=/run/meshtastic/gps_feed,mode=666
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
</span><span class="no">EOF

</span><span class="c"># GPS pipe feeder</span>
<span class="nb">sudo tee</span> /etc/systemd/system/meshtastic-gpspipe.service <span class="o">&gt;</span> /dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
[Unit]
Description=Feed NMEA from gpsd into Meshtastic PTY
Requires=meshtastic-pty.service gpsd.service
After=meshtastic-pty.service gpsd.service

[Service]
Type=simple
# Wait until the PTY appears and is a char device; then stream raw NMEA
ExecStart=/bin/sh -lc 'while [ ! -c /run/meshtastic/gps_feed ]; do sleep 0.5; done; exec /usr/bin/env gpspipe -r &gt; /run/meshtastic/gps_feed'
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
</span><span class="no">EOF

</span><span class="c">#</span>
<span class="c"># End of services setup</span>
<span class="c">#</span>

<span class="c"># Enable and startup the meshtastic GPS services</span>
<span class="nb">sudo </span>systemctl daemon-reload
<span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> meshtastic-pty.service
<span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> meshtastic-gpspipe.service
<span class="nb">sudo </span>systemctl start <span class="nt">--now</span> meshtastic-pty.service
<span class="nb">sudo </span>systemctl start <span class="nt">--now</span> meshtastic-gpspipe.service
systemctl <span class="nt">--no-pager</span> status meshtastic-pty meshtastic-gpspipe

<span class="c"># troubleshooting - you should see gps_feed and gps_in </span>
<span class="nb">ls</span> <span class="nt">-l</span> /run/meshtastic

<span class="c"># check for gps data - you should see a stream of NMEA traffic</span>
<span class="nb">sudo timeout </span>3 <span class="nb">cat</span> /run/meshtastic/gps_in

<span class="c"># Log check - make sure no errors</span>
journalctl <span class="nt">-u</span> meshtastic-pty <span class="nt">-b</span> <span class="nt">--no-pager</span>

<span class="c"># point meshtasticd at the new serial feed</span>
<span class="nb">sudo </span>nano /etc/meshtasticd/config.yaml

<span class="c"># Change /dev/ttyS0 to /run/meshtastic/gps_in , be careful of the spacing. YAML is sensitive about it. </span>
<span class="c"># Example:</span>
<span class="c">#GPS:</span>
<span class="c">#  SerialPath: /run/meshtastic/gps_in</span>

<span class="c">#</span>
<span class="c"># Meshtastic gps done </span>
<span class="c">#</span>


<span class="c"># Connect GPS to system timeout</span>
<span class="nb">sudo </span>nano /etc/chrony/chrony.conf
<span class="c"># add to end:</span>
<span class="c">## GPS via gpsd</span>
<span class="c">#refclock SHM 0 offset 0.0 delay 0.0 refid GPS poll 4</span>
<span class="c">## Adjust offset or delay as needed </span>

<span class="c"># Turn on the service </span>
<span class="c"># </span>
<span class="nb">sudo </span>systemctl restart chrony
<span class="nb">sudo </span>systemctl restart gpsd

<span class="c"># Troubleshooting</span>
<span class="c">#</span>
<span class="c"># Run this and make sure it's filled with tons of updating numbers</span>
cgps <span class="nt">-s</span>
<span class="c"># Check and see if you're getting GPS time data</span>
chronyc sources <span class="nt">-v</span>

</code></pre></div></div>

<h3 id="pps">PPS?</h3>

<p>Quectel EG25-G does partially support PPS, which allows really accurate time keeping. The system board has a wire to both the PCI-E connectors.</p>

<p>You can also run a jumper from the pad to one of the GPIO pins. It wouldn’t be very high precision like nanoseconds. This would bring down time inaccuracy to a few ms.</p>

<h3 id="alternative-setup">Alternative setup</h3>

<p>https://www.waveshare.com/wiki/EG25-G_mPCIe#How_to_Install_and_Use_Dial-up_Tool_.28Required_for_module_usage.29</p>

<p>Waveshare has an install script you can go with. I haven’t tried it out yet.</p>

<h3 id="making-sure-the-modem-gps-is-turned-on">Making sure the modem GPS is turned on</h3>

<p>Make sure flock is installed, you can check with sudo apt install util-linux</p>

<p>Write the following with sudo nano /usr/local/bin/check_mm_location.sh</p>

<p>This is still a work in progress.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># Ensure ModemManager location features are on for all detected modems.</span>
<span class="c"># Requires: mmcli, flock. Run as root (cron/systemd).</span>

<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="nv">LOG_FILE</span><span class="o">=</span><span class="s2">"/var/log/mm-location-monitor.log"</span>
<span class="nv">MMCLI</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">MMCLI</span><span class="k">:-</span><span class="p">/usr/bin/mmcli</span><span class="k">}</span><span class="s2">"</span>
<span class="nv">DATE</span><span class="o">=</span><span class="s2">"/bin/date"</span>
<span class="nv">FLOCK</span><span class="o">=</span><span class="s2">"/usr/bin/flock"</span>
<span class="nv">LOCKFILE</span><span class="o">=</span><span class="s2">"/var/lock/mm-location-monitor.lock"</span>

log<span class="o">()</span> <span class="o">{</span>
  <span class="nb">echo</span> <span class="s2">"</span><span class="si">$(</span><span class="nv">$DATE</span> <span class="s1">'+%Y-%m-%d %H:%M:%S'</span><span class="si">)</span><span class="s2">  </span><span class="nv">$*</span><span class="s2">"</span> | <span class="nb">tee</span> <span class="nt">-a</span> <span class="s2">"</span><span class="nv">$LOG_FILE</span><span class="s2">"</span>
<span class="o">}</span>

list_modem_ids<span class="o">()</span> <span class="o">{</span>
  <span class="c"># Output: one modem ID per line (e.g., 0, 1, 2 ...), or nothing if none exist</span>
  <span class="c"># mmcli -L example line:</span>
  <span class="c"># /org/freedesktop/ModemManager1/Modem/1 [QUALCOMM INCORPORATED] QUECTEL Mobile Broadband Module</span>
  <span class="s2">"</span><span class="nv">$MMCLI</span><span class="s2">"</span> <span class="nt">-L</span> 2&gt;/dev/null | <span class="nb">sed</span> <span class="nt">-En</span> <span class="s1">'s#.*/Modem/([0-9]+).*#\1#p'</span>
<span class="o">}</span>

check_and_fix_modem<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span><span class="nv">MID</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>

  <span class="c"># Verify the modem exists</span>
  <span class="k">if</span> <span class="o">!</span> <span class="s2">"</span><span class="nv">$MMCLI</span><span class="s2">"</span> <span class="nt">-m</span> <span class="s2">"</span><span class="nv">$MID</span><span class="s2">"</span> <span class="o">&gt;</span>/dev/null 2&gt;&amp;1<span class="p">;</span> <span class="k">then
    </span>log <span class="s2">"modem </span><span class="nv">$MID</span><span class="s2">: not present; skipping"</span>
    <span class="k">return </span>0
  <span class="k">fi</span>

  <span class="c"># Read location status</span>
  <span class="nb">local </span>STATUS
  <span class="k">if</span> <span class="o">!</span> <span class="nv">STATUS</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="s2">"</span><span class="nv">$MMCLI</span><span class="s2">"</span> <span class="nt">-m</span> <span class="s2">"</span><span class="nv">$MID</span><span class="s2">"</span> <span class="nt">--location-status</span> 2&gt;&amp;1<span class="si">)</span><span class="s2">"</span><span class="p">;</span> <span class="k">then
    </span>log <span class="s2">"modem </span><span class="nv">$MID</span><span class="s2">: failed to read location-status"</span>
    <span class="k">return </span>1
  <span class="k">fi</span>

  <span class="c"># Quick capability check; if modem doesn't support gps-unmanaged, don't spam errors</span>
  <span class="nb">local </span>CAP_LINE
  <span class="nv">CAP_LINE</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$STATUS</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'^\s*\|\s*capabilities:'</span> <span class="o">||</span> <span class="nb">true</span><span class="si">)</span><span class="s2">"</span>
  <span class="k">if</span> <span class="o">!</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$CAP_LINE</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-q</span> <span class="s1">'gps-unmanaged'</span><span class="p">;</span> <span class="k">then
    </span>log <span class="s2">"modem </span><span class="nv">$MID</span><span class="s2">: no gps-unmanaged capability; skipping gps enable"</span>
  <span class="k">fi</span>

  <span class="c"># Parse "enabled" and "signals"</span>
  <span class="nb">local </span>ENABLED_LINE SIGNALS_LINE
  <span class="nv">ENABLED_LINE</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$STATUS</span><span class="s2">"</span>  | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'^\s*\|\s*enabled:'</span>  <span class="o">||</span> <span class="nb">true</span><span class="si">)</span><span class="s2">"</span>
  <span class="nv">SIGNALS_LINE</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$STATUS</span><span class="s2">"</span>  | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'^\s*\|\s*signals:'</span>  <span class="o">||</span> <span class="nb">true</span><span class="si">)</span><span class="s2">"</span>

  <span class="nb">local </span><span class="nv">HAS_GPS_UNMANAGED</span><span class="o">=</span><span class="s2">"no"</span> <span class="nv">HAS_SIGNALS</span><span class="o">=</span><span class="s2">"no"</span>
  <span class="k">if </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$ENABLED_LINE</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-q</span> <span class="s1">'gps-unmanaged'</span><span class="p">;</span> <span class="k">then </span><span class="nv">HAS_GPS_UNMANAGED</span><span class="o">=</span><span class="s2">"yes"</span><span class="p">;</span> <span class="k">fi
  if </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$SIGNALS_LINE</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-q</span> <span class="s1">'yes'</span><span class="p">;</span> <span class="k">then </span><span class="nv">HAS_SIGNALS</span><span class="o">=</span><span class="s2">"yes"</span><span class="p">;</span> <span class="k">fi

  </span><span class="nb">local </span><span class="nv">changed</span><span class="o">=</span><span class="s2">"no"</span>

  <span class="c"># Enable gps-unmanaged if missing and supported</span>
  <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$HAS_GPS_UNMANAGED</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"yes"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$CAP_LINE</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-q</span> <span class="s1">'gps-unmanaged'</span><span class="p">;</span> <span class="k">then
    if</span> <span class="s2">"</span><span class="nv">$MMCLI</span><span class="s2">"</span> <span class="nt">-m</span> <span class="s2">"</span><span class="nv">$MID</span><span class="s2">"</span> <span class="nt">--location-enable-gps-unmanaged</span> <span class="o">&gt;</span>/dev/null 2&gt;&amp;1<span class="p">;</span> <span class="k">then
      </span>log <span class="s2">"modem </span><span class="nv">$MID</span><span class="s2">: enabled gps-unmanaged"</span>
      <span class="nv">changed</span><span class="o">=</span><span class="s2">"yes"</span>
    <span class="k">else
      </span>log <span class="s2">"modem </span><span class="nv">$MID</span><span class="s2">: FAILED to enable gps-unmanaged"</span>
    <span class="k">fi
  fi</span>

  <span class="c"># Enable signals if missing (try both flags for compatibility)</span>
  <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$HAS_SIGNALS</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"yes"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    if</span> <span class="s2">"</span><span class="nv">$MMCLI</span><span class="s2">"</span> <span class="nt">-m</span> <span class="s2">"</span><span class="nv">$MID</span><span class="s2">"</span> <span class="nt">--location-enable-signals</span> <span class="o">&gt;</span>/dev/null 2&gt;&amp;1<span class="p">;</span> <span class="k">then
      </span>log <span class="s2">"modem </span><span class="nv">$MID</span><span class="s2">: enabled signals"</span>
      <span class="nv">changed</span><span class="o">=</span><span class="s2">"yes"</span>
    <span class="k">elif</span> <span class="s2">"</span><span class="nv">$MMCLI</span><span class="s2">"</span> <span class="nt">-m</span> <span class="s2">"</span><span class="nv">$MID</span><span class="s2">"</span> <span class="nt">--location-set-enable-signal</span> <span class="o">&gt;</span>/dev/null 2&gt;&amp;1<span class="p">;</span> <span class="k">then
      </span>log <span class="s2">"modem </span><span class="nv">$MID</span><span class="s2">: enabled signals (compat flag)"</span>
      <span class="nv">changed</span><span class="o">=</span><span class="s2">"yes"</span>
    <span class="k">else
      </span>log <span class="s2">"modem </span><span class="nv">$MID</span><span class="s2">: FAILED to enable signals"</span>
    <span class="k">fi
  fi</span>

  <span class="c"># Dump refreshed status if anything changed</span>
  <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$changed</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"yes"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="s2">"</span><span class="nv">$MMCLI</span><span class="s2">"</span> <span class="nt">-m</span> <span class="s2">"</span><span class="nv">$MID</span><span class="s2">"</span> <span class="nt">--location-status</span> | <span class="nb">sed</span> <span class="s1">'s/^/modem '</span><span class="s2">"</span><span class="nv">$MID</span><span class="s2">"</span><span class="s1">': /'</span> | <span class="nb">tee</span> <span class="nt">-a</span> <span class="s2">"</span><span class="nv">$LOG_FILE</span><span class="s2">"</span> <span class="o">&gt;</span>/dev/null
  <span class="k">else
    </span>log <span class="s2">"modem </span><span class="nv">$MID</span><span class="s2">: already OK (gps-unmanaged + signals)"</span>
  <span class="k">fi</span>
<span class="o">}</span>

_main_run<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span><span class="nv">any</span><span class="o">=</span>0
  <span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> MID<span class="p">;</span> <span class="k">do
    </span><span class="nv">any</span><span class="o">=</span>1
    check_and_fix_modem <span class="s2">"</span><span class="nv">$MID</span><span class="s2">"</span>
  <span class="k">done</span> &lt; &lt;<span class="o">(</span>list_modem_ids<span class="o">)</span>

  <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$any</span><span class="s2">"</span> <span class="nt">-eq</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span>log <span class="s2">"no modems detected by ModemManager"</span>
  <span class="k">fi</span>
<span class="o">}</span>

main<span class="o">()</span> <span class="o">{</span>
  <span class="c"># Serialize concurrent runs</span>
  <span class="nb">exec</span> <span class="s2">"</span><span class="nv">$FLOCK</span><span class="s2">"</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$LOCKFILE</span><span class="s2">"</span> <span class="nt">-c</span> <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span> _run 2&gt;/dev/null <span class="o">||</span> <span class="nb">exit </span>0
<span class="o">}</span>

<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">1</span><span class="k">:-}</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"_run"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span>_main_run
<span class="k">else
  </span>main
<span class="k">fi</span>
</code></pre></div></div>

<p>You can set it up with</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo chmod </span>755 /usr/local/bin/check_mm_location.sh
<span class="nb">sudo chmod</span> +x /usr/local/bin/check_mm_location.sh
<span class="nb">sudo touch</span> /var/log/mm-location-monitor.log
<span class="nb">sudo chown </span>root:adm /var/log/mm-location-monitor.log 2&gt;/dev/null <span class="o">||</span> <span class="nb">true
sudo </span>crontab <span class="nt">-e</span>
<span class="c"># 0 * * * * /usr/local/bin/check_mm_location.sh</span>

</code></pre></div></div>

<h3 id="cheap-lte-service">Cheap LTE service</h3>

<p>Embedded Works slash IoTDataWorks sells an unlimited 64kbps SIM for $55 ish on <a href="https://www.amazon.com/dp/B07JCTZ3BF">Amazon</a>. I was told it didn’t work with the Nebra or Nebra cell modem but worked for me. This should work, but I haven’t tested it on a fresh install yet. It only uses T-Mobile, operator ID 310260</p>

<p>Embedded Works uses m2mglobal as the APN. Be sure to reboot a couple times to make sure it stays working. Might take a minute or two to connect.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Enable services + radio</span>
<span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> ModemManager NetworkManager
<span class="c">#sudo nmcli radio wwan on</span>
<span class="c">#sudo rfkill unblock all</span>
<span class="nb">sudo </span>mmcli <span class="nt">-m</span> any <span class="nt">--enable</span>
<span class="c">#sudo mmcli -m any --simple-connect="apn=m2mglobal,ip-type=ipv4"</span>
<span class="c">#sudo mmcli -m any --3gpp-register-in-operator=310260</span>

<span class="nb">sudo </span>nmcli connection add <span class="nb">type </span>gsm ifname <span class="s2">"*"</span> con-name LTE autoconnect <span class="nb">yes</span> <span class="se">\</span>
    apn m2mglobal <span class="se">\</span>
    connection.autoconnect <span class="nb">yes</span> <span class="se">\</span>
    ipv4.method auto

<span class="nb">sudo </span>nmcli connection modify LTE gsm.network-id 310260
<span class="nb">sudo </span>reboot

<span class="c"># Wait a couple minutes and then run the --verbose to make sure everything is correct</span>
<span class="c"># check ip route show and ifconfig as well</span>

</code></pre></div></div>]]></content><author><name>Casper</name></author><category term="Meshtastic" /><category term="LoRA" /><category term="crypto" /><summary type="html"><![CDATA[Low cost high power LoRA Mesh networking]]></summary></entry><entry><title type="html">Recycling Old Crypto Miners - Nebra</title><link href="/Recycling-Old-Crypto-Miners/" rel="alternate" type="text/html" title="Recycling Old Crypto Miners - Nebra" /><published>2025-08-12T00:00:00+00:00</published><updated>2025-08-23T20:00:00+00:00</updated><id>/Recycling-Old-Crypto-Miners</id><content type="html" xml:base="/Recycling-Old-Crypto-Miners/"><![CDATA[<p><img src="/images/posts/nebra/nebra-pi.png" alt="meshtastic Pi" /></p>

<h3 id="recycling-old-crypto-miners-for-something-useful">Recycling Old Crypto Miners for Something Useful</h3>

<p>Helium Coin spiked and crashed, leaving many $750+ miners useless. Rather than becoming e-waste, they can be repurposed for Meshtastic. The most practical model I’ve found is the <a href="https://helium.Nebra.com/pdfs/outdoor-overview.pdf">Nebra Outdoor Hotspot Miner</a>.</p>

<p>Though a Pi CM4 would be nice, cost, power and heat likely dictated the CM3. Waveshare does make a CM4-to-CM3 adapter if you need more processing horsepower but there’s already power issues with the Nebra. I’ve ordered other outdoor miners to test for easy conversions as well. So far Nebra is king of the hill.</p>

<h3 id="hardware-details">Hardware Details</h3>

<p>Each kit includes a 915 MHz 3 dBi antenna, a 2.4 GHz antenna, an aluminum enclosure, mounting hardware, a Pi CM3 board with USB WiFi and Bluetooth. Units are often found on eBay for about $50; if listed higher, try offering around that price. They need 12 VDC or PoE (12–15 W draw), which is too high for USB and not ideal for solar.</p>

<p><a href="https://mtnme.sh/">Mountain Mesh</a> in Georgia offers accessories: Nebra Pi hats for 40-pin Pis, the <a href="https://mtnme.sh/devices/MeshToad/">MESHTOAD</a> USB for any PC, and a still-prototypw PCI-E card. Join their <a href="https://discord.gg/4WN32RHGSs">Discord</a> for details. The enclosure is a <a href="https://www.alibaba.com/product-detail/DAM005C-210-130-50mm-aluminium-IP67_1600234767148.html">DAM005C</a> from Ningbo Darer, claimed IP65/IP67, about $15–20 plus shipping. A schematic for enclosure is <a href="https://forum.digikey.com/uploads/short-url/jdaai1wYIySMZllj9n3FrCqtYtU.pdf">here</a>.</p>

<h3 id="shucking">Shucking</h3>

<p>Remove the USB board if you don’t need it. The Bluetooth adapter has short range and no external antenna support, I believe it was only meant for initial setup. Keep WiFi and connect to the single USB port. Keep an eye on bulkhead thickness for board clearance. Mounting posts are all M3 pan head screws, buy M3 washers for grounding. Then attach the WeHooper Nebra hat to the 40-pin header or other meshtastic radio.</p>

<p>The eMMC module is a small ‘key’ with gold dot near the Pi board. It will work in a MicroSD slot. Adapter compatibility varies greatly; SanDisk SD card adapters don’t work, uGreen USB MicroSD adapter off Amazon work reliably. eMMC has much longer lifespan and operating temps, try not to use a MicroSD card for anything but testing or temp controlled environment.</p>

<p>There’s two ways of writing the OS:</p>

<h3 id="writing-debian-using-nebra-pi-board">Writing Debian Using Nebra Pi board</h3>

<p>Still confirming. Looks like it runs much slower but more reliably. The two pin connector tells the board to either be in programming mode or operating mode. The three pin connector is flashing firmware or writing to SD/eMMC card, I believe.</p>

<ul>
  <li>Remove Nebra Pi board from Nebra, remove Micro-USB cable</li>
  <li>Download and install <a href="https://github.com/raspberrypi/usbboot/raw/master/win32/rpiboot_setup.exe">Raspberry Pi USB Boot</a> before anything else</li>
  <li>There are two sets of jumpers on Nebra Pi board right next to the CM3. Leave 3 pin connector alone if already on Pins 2 and 3 (. On 2 pin connector, switch from not jumped to jumped</li>
  <li>Plug in USB cable to PC and Pi board</li>
  <li>Run rpi-boot-CM-CM2-CM3</li>
  <li>Run Pi Imager</li>
  <li>Once done, unjump 2 pin connector! Will not work unless unjumpered. Plug back into nebra, leave 3 pin alone.</li>
</ul>

<h3 id="writing-debian-microsd-adapter-method">Writing Debian MicroSD adapter method</h3>

<ul>
  <li>Remove eMMC key, put in MicroSD slot or adapter</li>
  <li>Fire up <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a>.</li>
  <li>First button: Pi 3 (Nebra uses a Compute Module 3 (CM3) which is in the small text)</li>
  <li>Second button: Go with Lite 64 bit, less running services</li>
  <li>Third button: should come up with 32GB option, select that.</li>
</ul>

<p>Once you’re done, hit the bottom right button to start. You’ll get a prompt about settings. Go ahead and edit settings. Set hostname, WiFi, account, sshd, etc etc. Otherwise you won’t be able to log into your Pi afterwards. Reinstall the module, power up, and <a href="https://www.chiark.greenend.org.uk/~sgtatham/putty/">SSH</a> in. To find your new node, do a port scan or check your router for the DHCP entry.</p>

<p>To install Meshtastic:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># New system updates</span>
<span class="nb">sudo </span>apt update <span class="nt">-y</span>
<span class="nb">sudo </span><span class="nv">DEBIAN_FRONTEND</span><span class="o">=</span>noninteractive <span class="se">\</span>
apt-get <span class="nt">-y</span> <span class="se">\</span>
  <span class="nt">-o</span> Dpkg::Options::<span class="o">=</span><span class="s2">"--force-confdef"</span> <span class="se">\</span>
  <span class="nt">-o</span> Dpkg::Options::<span class="o">=</span><span class="s2">"--force-confold"</span> <span class="se">\</span>
  dist-upgrade
<span class="c"># Meshtastic install</span>
<span class="nb">echo</span> <span class="s1">'deb http://download.opensuse.org/repositories/network:/Meshtastic:/beta/Debian_13/ /'</span> | <span class="nb">sudo tee</span> /etc/apt/sources.list.d/network:Meshtastic:beta.list
curl <span class="nt">-fsSL</span> https://download.opensuse.org/repositories/network:Meshtastic:beta/Debian_13/Release.key | gpg <span class="nt">--dearmor</span> | <span class="nb">sudo tee</span> /etc/apt/trusted.gpg.d/network_Meshtastic_beta.gpg <span class="o">&gt;</span> /dev/null
<span class="nb">sudo </span>apt update <span class="nt">-y</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>meshtasticd i2c-tools <span class="nt">-y</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>pipx <span class="nt">-y</span> <span class="o">&amp;&amp;</span> pipx <span class="nb">install</span> <span class="s2">"meshtastic[cli]"</span>
pipx ensurepath
<span class="nb">sudo </span>nano /boot/firmware/config.txt

<span class="c"># add or uncomment these lines</span>
<span class="c">#dtparam=i2c_arm=on</span>
<span class="c">#dtparam=spi=on</span>
<span class="c">#dtoverlay=spi0-0cs</span>

<span class="c"># Turns on auto-discovery</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>avahi-daemon

<span class="nb">sudo tee</span> /etc/avahi/services/meshtastic.service <span class="o">&gt;</span> /dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
&lt;?xml version="1.0" standalone="no"?&gt;&lt;!--*-nxml-*--&gt;
&lt;!DOCTYPE service-group SYSTEM "avahi-service.dtd"&gt;
&lt;service-group&gt;
    &lt;name&gt;Meshtastic&lt;/name&gt;
    &lt;service protocol="ipv4"&gt;
        &lt;type&gt;_meshtastic._tcp&lt;/type&gt;
        &lt;port&gt;4403&lt;/port&gt;
    &lt;/service&gt;
&lt;/service-group&gt;
</span><span class="no">EOF

</span><span class="nb">sudo </span>systemctl <span class="nb">enable </span>avahi-daemon <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>systemctl start avahi-daemon


<span class="c"># Rename hostname, set for DNS</span>
<span class="nb">sudo </span>hostnamectl set-hostname KD3BQB-NodeXX
<span class="nb">sudo </span>systemctl restart avahi-daemon
<span class="c"># sudo nano /etc/hosts</span>

<span class="c"># if ZebraHat, enable i2c via this</span>
<span class="c">#sudo raspi-config</span>

<span class="c"># Meshtastic CLI</span>
meshtastic <span class="nt">--host</span> <span class="nt">--set</span> lora.region <span class="s2">"US"</span>
meshtastic <span class="nt">--host</span> <span class="nt">--set-owner</span> <span class="s2">"SUSQ VAL PA Mesh - Town - Tower"</span>
meshtastic <span class="nt">--host</span> <span class="nt">--set-owner-short</span> <span class="s2">"SVMI"</span>
meshtastic <span class="nt">--host</span>  <span class="nt">--export-config</span> | <span class="nb">grep</span> <span class="s2">"Key:"</span>

</code></pre></div></div>

<p>Enable SPI in <code class="language-plaintext highlighter-rouge">/boot/firmware/config.txt</code> and ensure <code class="language-plaintext highlighter-rouge">dtoverlay=spi0-0cs</code> is present. Optionally enable I2C via dtparam=i2c_arm=on and install <code class="language-plaintext highlighter-rouge">i2c-tools</code>. Reboot, then download the correct hat config:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c"># Documentation at https://github.com/wehooper4/meshtastic-Hardware/tree/main/NebraHat</span>

<span class="c"># Get YAML</span>
<span class="c"># sudo wget –O /etc/meshtasticd/config.d/NebraHat_1W.yaml https://github.com/wehooper4/meshtastic-Hardware/raw/refs/heads/main/NebraHat/NebraHat_1W.yaml</span>
<span class="nb">sudo </span>wget <span class="nt">-O</span> /etc/meshtasticd/config.d/NebraHat_2W.yaml https://github.com/wehooper4/meshtastic-Hardware/raw/refs/heads/main/NebraHat/NebraHat_2W.yaml
<span class="c"># sudo wget -O /etc/meshtasticd/config.d/NebraHat_Duo_E22P.yaml https://raw.githubusercontent.com/wehooper4/Meshtastic-Hardware/refs/heads/main/NebraHat/Duo/NebraHat_Duo_E22P.yaml</span>
<span class="c">#sudo wget –O /etc/meshtasticd/config.d/ZebraHat.yaml https://raw.githubusercontent.com/wehooper4/Meshtastic-Hardware/refs/heads/main/ZebraHAT/ZebraHat.yaml</span>


<span class="c"># Edit YAML</span>
<span class="c">#sudo nano /etc/meshtasticd/config.d/NebraHat_1W.yaml</span>
<span class="nb">sudo </span>nano /etc/meshtasticd/config.d/NebraHat_2W.yaml
<span class="c">#sudo nano /etc/meshtasticd/config.d/NebraHat_Duo_E22P.yaml</span>
<span class="c">#sudo nano /etc/meshtasticd/config.d/ZebraHat.yaml</span>

<span class="c"># If 2W, verify power level is set to 8 or lower. 4 is recommended due to 5v rail sag, and is NOT cutting your TX power in half. </span>
<span class="c"># Obviously change 2W to 1W if purchased that model. </span>

<span class="c"># Edit Meshtasticd Config</span>
<span class="c"># If you have problems below such as "No sx1262 radio", try uncommenting the CS line</span>
<span class="nb">sudo </span>nano /etc/meshtasticd/config.yaml

<span class="c"># Fire up meshtasticd </span>
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>meshtasticd <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>systemctl start meshtasticd

</code></pre></div></div>

<p>Edit config.yaml to set MAC (use MACAddressSource eth0) and node limits (200-400). Troubleshoot with <code class="language-plaintext highlighter-rouge">journalctl -xeu meshtasticd.service</code> or run <code class="language-plaintext highlighter-rouge">meshtasticd</code> manually. Configure through the Meshtastic app. Use the Network option on the Cloud tab in the app.</p>

<h3 id="sensors-and-gps">Sensors and GPS</h3>

<p><img src="/images/posts/nebra/nebra-gps-neo6m.jpg" alt="NEO-6M" /></p>

<p>If using I2C sensors, enable I2C by uncommenting their lines in both <code class="language-plaintext highlighter-rouge">/boot/firmware/config.txt</code> and <code class="language-plaintext highlighter-rouge">/etc/meshtasticd/config.yaml</code>. Check devices with <code class="language-plaintext highlighter-rouge">i2cdetect -y 1</code></p>

<p>Some miners include a NEO-6M GPS chip, which is nice. If absent, adding surface mount parts is possible but often a USB GPS or cell modem is easier. I spoke with the developer of the Nebra board. The pin allocation on the hat header is because he wanted to leave UART0 available on 14/15. To enable onboard NEO-6M GPS:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>gpsd gpsd-clients chrony socat <span class="nt">-y</span>
<span class="nb">sudo </span>nano /boot/firmware/config.txt
<span class="c"># Add to end of file:</span>
<span class="c"># enable_uart=1</span>
<span class="c"># dtoverlay=uart1,txd1_pin=32,rxd1_pin=33,pin_func=7</span>
<span class="c">#sudo raspi-config   # Select (3) serial interface -&gt; I6 Serial Port -&gt; disable shell serial console -&gt; enable serial hardware</span>
<span class="nb">sudo </span>raspi-config nonint do_serial_hw 0 <span class="c"># Enable Serial Port (enable_uart=1)</span>
<span class="nb">sudo </span>raspi-config nonint do_serial_cons 1 <span class="c"># Disable Serial Console</span>
<span class="nb">sudo </span>nano /etc/default/gpsd
<span class="c"># DEVICES="/dev/serial1"</span>
<span class="nb">sudo </span>nano /etc/chrony/chrony.conf
<span class="c"># # Use gpsd as a time source</span>
<span class="c"># refclock SHM 0 refid GPS precision 1e-1 offset 0.0 delay 0.2</span>
<span class="nb">sudo </span>systemctl restart chrony
<span class="nb">sudo </span>reboot

<span class="c"># check sats</span>
cgps <span class="nt">-s</span>
<span class="c"># Once you have satellite lock, check and see if you're getting GPS time data</span>
chronyc sources <span class="nt">-v</span>

</code></pre></div></div>

<p>Update <code class="language-plaintext highlighter-rouge">/etc/default/gpsd</code> with DEVICES=”/dev/serial1” and add <code class="language-plaintext highlighter-rouge">/dev/serial1</code> to the Meshtastic GPS: section of config.yaml.</p>

<h3 id="grounding">Grounding</h3>

<p><img src="/images/posts/nebra/nebra_case_grounding.jpg" alt="Nebra Grounding" /></p>

<p>Critical if placed on a tower, especially with RF equipment.</p>

<p>Rip out the USB board, Pi CM3 board and main board. Shuck a $12 Ubiquiti ETH-SP-G2 Surge Protector. Pry it out with a multi-tool, bending the case a bit is fine. It is not meant to stop a lightning strike. It’s meant to ensure Ethernet pins potential vs the local enclosure ground never exceeds 90-100v and preventing surges/transient power. ETH-SP-G2 does want to stick up a bit. Screw unit to a post with M3 pan head screw and washer, use a cheap flathead screwdriver as a chisel, give it some light taps until closer to the floor of the case. Put some silicone tape on top, to prevent ground shorts from the system board.</p>

<p>Use Noalox on aluminum wiring points, helps prevent oxidation.</p>

<p>Use 12AWG solid core grounding wire, crimped (not soldered) to a ring terminator. Check with your tower owner if heavier gauge wire is needed. Use one of the M10 cable gland. It’s pretty good match for the 12AWG wire.</p>

<p>Put a lightning arrestor on the main antenna. Connect both wires with a split bolt and then to tower ground using ground clamp. Notion is to provide a low-impedance path to ground.</p>

<h3 id="cell-modem">Cell Modem</h3>

<p><img src="/images/posts/nebra/nebra-case.jpg" alt="meshtastic cell modem" /></p>

<p>For remote access, see the <a href="https://casper.im/Nebra-Cell-Modem/">Quectel EG25-G Mini PCIe guide</a>.</p>

<p>This LTE PCI-E card supports multiple GNSS constellations and quite a few useful features. Use MAIN only for Meshtastic to avoid interference. Use other LTE antenna for GPS, it works fine. The EG25-G requires a SIM; eSIM is possible but complex. GPS performance was good even indoors. Keep LTE and LoRa antennas separated on opposite sides of the case.</p>

<h3 id="wifi-ap">WiFi AP</h3>

<p>Buy a new WiFi dongle that includes native linux support and AP mode. Included one has driver issues, and is kinda nightmare.</p>

<p>Adding WiFi network manually</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmcli connection add <span class="nb">type </span>wifi ifname wlan0 con-name mywifi ssid <span class="s2">"SSID_NAME"</span>
<span class="nb">sudo </span>nmcli connection modify mywifi wifi-sec.key-mgmt wpa-psk
<span class="nb">sudo </span>nmcli connection modify mywifi wifi-sec.psk <span class="s2">"YOUR_PASSWORD"</span>
<span class="nb">sudo </span>nmcli connection modify mywifi connection.autoconnect <span class="nb">yes</span>
</code></pre></div></div>

<p>Setting up AP mode</p>

<p>Save to setup-ap.sh and run sudo bash ./setup-ap.sh</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># Simple AP setup script for Debian</span>
<span class="c"># Run as root</span>

<span class="c">### CONFIGURATION ###</span>
<span class="nv">AP_SSID</span><span class="o">=</span><span class="s2">"MyAccessPoint"</span>
<span class="nv">AP_PASSWORD</span><span class="o">=</span><span class="s2">"SuperSecret123"</span>
<span class="nv">WLAN_IF</span><span class="o">=</span><span class="s2">"wlan0"</span>
<span class="nv">AP_IP</span><span class="o">=</span><span class="s2">"192.168.50.1"</span>
<span class="nv">DHCP_RANGE_START</span><span class="o">=</span><span class="s2">"192.168.50.10"</span>
<span class="nv">DHCP_RANGE_END</span><span class="o">=</span><span class="s2">"192.168.50.200"</span>
<span class="nv">UPLINK_IF</span><span class="o">=</span><span class="s2">"eth0"</span>   <span class="c"># or wwan0 if you want to NAT to LTE</span>
<span class="c">####################</span>

<span class="nb">set</span> <span class="nt">-e</span>

<span class="nb">echo</span> <span class="s2">"[*] Installing required packages..."</span>
<span class="nb">sudo </span>apt <span class="nt">-o</span> Acquire::ForceIPv4<span class="o">=</span><span class="nb">true </span>update
<span class="nb">sudo </span>apt <span class="nt">-o</span> Acquire::ForceIPv4<span class="o">=</span><span class="nb">true install</span> <span class="nt">-y</span> hostapd dnsmasq iptables-persistent ifupdown

<span class="nb">echo</span> <span class="s2">"[*] Stopping services until configured..."</span>
systemctl stop hostapd <span class="o">||</span> <span class="nb">true
</span>systemctl stop dnsmasq <span class="o">||</span> <span class="nb">true

echo</span> <span class="s2">"[*] Configuring static IP on </span><span class="nv">$WLAN_IF</span><span class="s2">..."</span>
<span class="nb">cat</span> <span class="o">&gt;</span>/etc/network/interfaces.d/<span class="nv">$WLAN_IF</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
auto </span><span class="nv">$WLAN_IF</span><span class="sh">
iface </span><span class="nv">$WLAN_IF</span><span class="sh"> inet static
    address </span><span class="nv">$AP_IP</span><span class="sh">
    netmask 255.255.255.0
</span><span class="no">EOF
</span>ifdown <span class="nv">$WLAN_IF</span> <span class="o">||</span> <span class="nb">true
</span>ifup <span class="nv">$WLAN_IF</span>

<span class="nb">echo</span> <span class="s2">"[*] Configuring dnsmasq..."</span>
<span class="nb">mv</span> /etc/dnsmasq.conf /etc/dnsmasq.conf.orig.<span class="si">$(</span><span class="nb">date</span> +%s<span class="si">)</span> <span class="o">||</span> <span class="nb">true
cat</span> <span class="o">&gt;</span>/etc/dnsmasq.conf <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
interface=</span><span class="nv">$WLAN_IF</span><span class="sh">
dhcp-range=</span><span class="nv">$DHCP_RANGE_START</span><span class="sh">,</span><span class="nv">$DHCP_RANGE_END</span><span class="sh">,255.255.255.0,24h
</span><span class="no">EOF

</span><span class="nb">echo</span> <span class="s2">"[*] Configuring hostapd..."</span>
<span class="nb">cat</span> <span class="o">&gt;</span>/etc/hostapd/hostapd.conf <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
interface=</span><span class="nv">$WLAN_IF</span><span class="sh">
driver=nl80211
ssid=</span><span class="nv">$AP_SSID</span><span class="sh">
hw_mode=g
channel=6
wmm_enabled=1
auth_algs=1
ignore_broadcast_ssid=0
wpa=2
wpa_passphrase=</span><span class="nv">$AP_PASSWORD</span><span class="sh">
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
</span><span class="no">EOF

</span><span class="c"># Point hostapd to config</span>
<span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s|^#DAEMON_CONF=.*|DAEMON_CONF=</span><span class="se">\"</span><span class="s2">/etc/hostapd/hostapd.conf</span><span class="se">\"</span><span class="s2">|"</span> /etc/default/hostapd

<span class="nb">echo</span> <span class="s2">"[*] Enabling IP forwarding..."</span>
<span class="nb">sed</span> <span class="nt">-i</span> <span class="s1">'s/^#\?net.ipv4.ip_forward.*/net.ipv4.ip_forward=1/'</span> /etc/sysctl.conf
sysctl <span class="nt">-p</span>

<span class="nb">echo</span> <span class="s2">"[*] Setting up NAT to </span><span class="nv">$UPLINK_IF</span><span class="s2">..."</span>
iptables <span class="nt">-t</span> nat <span class="nt">-A</span> POSTROUTING <span class="nt">-o</span> <span class="nv">$UPLINK_IF</span> <span class="nt">-j</span> MASQUERADE
iptables <span class="nt">-A</span> FORWARD <span class="nt">-i</span> <span class="nv">$UPLINK_IF</span> <span class="nt">-o</span> <span class="nv">$WLAN_IF</span> <span class="nt">-m</span> state <span class="nt">--state</span> RELATED,ESTABLISHED <span class="nt">-j</span> ACCEPT
iptables <span class="nt">-A</span> FORWARD <span class="nt">-i</span> <span class="nv">$WLAN_IF</span> <span class="nt">-o</span> <span class="nv">$UPLINK_IF</span> <span class="nt">-j</span> ACCEPT
netfilter-persistent save

<span class="nb">echo</span> <span class="s2">"[*] Enabling services..."</span>
systemctl unmask hostapd
systemctl <span class="nb">enable </span>hostapd
systemctl <span class="nb">enable </span>dnsmasq
systemctl restart hostapd
systemctl restart dnsmasq

<span class="nb">echo</span> <span class="s2">"[+] Access Point setup complete!"</span>
<span class="nb">echo</span> <span class="s2">"    SSID: </span><span class="nv">$AP_SSID</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"    Password: </span><span class="nv">$AP_PASSWORD</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"    Interface: </span><span class="nv">$WLAN_IF</span><span class="s2"> (</span><span class="nv">$AP_IP</span><span class="s2">)"</span>
</code></pre></div></div>

<h3 id="antenna-selection">Antenna Selection</h3>

<p>Stock antenna is decent at alleged 3dBi. RAK sells similiarly decent generic antennas. See <a href="https://github.com/meshtastic/antenna-reports">https://github.com/meshtastic/antenna-reports</a></p>

<p>Do not focus on just SWR. If you hooked up a 50 Ω resistor to your NanoVNA, Thanos would be happy that it perfectly balanced but it won’t radiate RF well. SWR tells you how well your antenna matches your transmitter, not antenna performance. High SWR does mean wasted battery and poor range, but low SWR doesn’t guarantee good transmitting or efficiency. To do that, hook up the antenna and take measurements at different distance and angle.</p>

<p>Gain is not magic. It’s not adding power, it’s shaping it. 0dBi would be a very fat (theoretical and idealized isotropic radiator) donut, handy if you want good coverage in all directions equally. 9dBi would be a very wide but thin pancake, handy if you put on a tower and want to reach other towers. 3-6dBi is compromising between the two.</p>

<p>Higher gain flattens the vertical beam, turning the donut into a wider, thinner pancake. There is no ideal, only ideal for your purpose.</p>

<p>If you want all-purpose coverage from a single mountaintop node, 5 dBi, ISM-tuned is the sweet spot. It won’t be great at distance, but it won’t leave nearby hikers without coverage either. Incidentally these tend to be expensive antennas.</p>

<p>If you want all-purchase coverage in an urban or suburban environment, 3 dBi would probably be a better choice.</p>

<p>Now let’s make things even more complicated. It’s not JUST the dBi. That shapes the power, but how do we get the power in the first place?</p>

<p>With commercial high quality fiberglass omnidirectional antennas, you’re paying the extra for lower conduction/dielectric loss (ohmic heating in conductors, dielectric materials, radome, etc.). High-quality commercial fiberglass omnis often have radiation efficiency &gt;90–95% (loss &lt;0.5 dB). Cheap eBay/Amazon antennas can be much worse — sometimes only 30–60% efficiency. Meaning if you’re beaming a watt, you might only be shooting out 300-600 milliwatts for that antenna to shape.</p>

<p>SWR tells you what fraction of power even makes it into the antenna.
Efficiency tells you how much of that delivered power is actually radiated vs lost as heat.
Together, they give you real radiated efficiency.</p>

<p>Let’s suppose you have a great antenna with SWR of 1.5, you’ll get a reflection ~4% of TX power and 92.5% antenna efficiency, ~88% of the original watt is radiated. Meaning you get around 0.88W transmitted.</p>

<p>Increase the SWR to 2, you’ll get a reflection ~11% of TX power and 92.5% antenna efficiency, ~82% of the original watt is radiated. Meaning you get around 0.82W transmitted.</p>

<p>Suppose you want to add coax instead of mounting your antenna to the ipex bulkhead. 1dB of feedline loss can cost you 20.6% of the watt before it even gets to your expensive commercial antenna. Taking that awesome 0.88W down to 0.71W.</p>

<h3 id="reliability">Reliability</h3>

<p>To auto-restart Meshtastic and reboot weekly:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo tee</span> /usr/local/bin/check_meshtasticd.sh <span class="o">&gt;</span> /dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
#!/bin/bash
SERVICE="meshtasticd"
if ! pgrep -x "</span><span class="nv">$SERVICE</span><span class="sh">" &gt; /dev/null; then
    echo "</span><span class="si">$(</span><span class="nb">date</span><span class="si">)</span><span class="sh">: </span><span class="nv">$SERVICE</span><span class="sh"> not running, restarting..." &gt;&gt; /var/log/meshtasticd_monitor.log
    systemctl restart </span><span class="nv">$SERVICE</span><span class="sh">
fi
</span><span class="no">EOF
</span><span class="nb">sudo chmod</span> +x /usr/local/bin/check_meshtasticd.sh
<span class="nb">sudo </span>crontab <span class="nt">-e</span>
<span class="c"># 0 * * * * /usr/local/bin/check_meshtasticd.sh</span>
<span class="c"># 0 1 * * 1 /sbin/reboot</span>
</code></pre></div></div>

<p>If you’re not very experienced with Linux, remember to log in every so often to run updates. You can automate that as well.</p>

<h3 id="remote-access">Remote Access</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nano /etc/modprobe.d/ipv6.conf
<span class="c">## Don't load ipv6 by default</span>
<span class="c">#alias net-pf-10 off</span>
<span class="c">#alias ipv6 off</span>

curl <span class="nt">-fsSL</span> https://tailscale.com/install.sh | sh
<span class="nb">sudo </span>tailscale up
</code></pre></div></div>

<p>Copy URL to web browser.</p>

<p>Once you have tailscale installed, you can also install tailscale on your phone and use the Meshtastic app to connect to your node from anywhere in the world.</p>

<h3 id="software-defined-radio-sdr">Software Defined Radio (SDR)</h3>

<p><img src="/images/posts/nebra/nebra-sdr.jpg" alt="NEO-6M" /></p>

<p>I installed a NooElec NESDR Nano 3 in several Nebra nodes. Notion is in high difficulty radio frequency environments, it can help look at what’s happening on the spectrum. Running meshtasticd and SDR software at the same time may make the Pi CM3 struggle, but we’ll see how it goes.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install apps</span>
<span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> rtl-sdr rtl-433 soapyremote-server

<span class="c"># should see: Bus 001 Device 004: ID 0bda:2838 Realtek Semiconductor Corp. RTL2838 DVB-T</span>
lsusb | <span class="nb">grep </span>RTL2838

<span class="nb">sudo tee</span> /etc/udev/rules.d/20-rtl-sdr.rules <span class="o">&gt;</span> /dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
# NooElec NESDR / RTL2832U SDR dongles
SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", MODE="0666", GROUP="plugdev"
</span><span class="no">EOF
</span><span class="nb">sudo </span>udevadm control <span class="nt">--reload-rules</span>
<span class="nb">sudo </span>udevadm trigger
<span class="nb">sudo </span>usermod <span class="nt">-aG</span> plugdev <span class="nv">$USER</span>

<span class="c"># test SDR</span>
rtl_test <span class="nt">-t</span>

<span class="c"># Test capture</span>
rtl_sdr <span class="nt">-s</span> 2048000 <span class="nt">-f</span> 100e6 <span class="nt">-n</span> 2048000 /tmp/fm.iq

<span class="c"># run SDR server</span>
rtl_tcp <span class="nt">-a</span> 0.0.0.0 <span class="nt">-p</span> 1234
<span class="c"># soapyremote-server</span>
</code></pre></div></div>

<p>Optionally you may want to blacklist the old drivers</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"blacklist dvb_usb_rtl28xxu
blacklist rtl2832
blacklist rtl2830"</span> | <span class="nb">sudo tee</span> /etc/modprobe.d/blacklist-rtlsdr.conf
<span class="nb">sudo </span>update-initramfs <span class="nt">-u</span>
<span class="nb">sudo </span>reboot
lsmod | <span class="nb">grep </span>rtl
</code></pre></div></div>
<p>You should not see dvb_usb_rtl28xxu, rtl2832, or rtl2830.
Your SDR software (rtl_test, rtl_tcp, etc.) should now directly claim the dongle without conflict.</p>

<h3 id="prepping-miner-for-outdoor-deployment">Prepping Miner for Outdoor Deployment</h3>

<ul>
  <li>The stock antenna is usable but can be upgraded.</li>
  <li>Wrap threads with telfon tape.</li>
  <li>Wrap all bulkheads with electrical tape, silicon tape, and then another layer of electrical tape.</li>
  <li>Alternatively you can use marine shrink tube or butyl tape like CoaxSeal</li>
  <li>Fit the EMI rope gasket into the upper lid, trim to fit.</li>
  <li>Use surge protection at both ends; put a shucked ETH-SP-G2 in the case and connect another cased ETH-SP-G2 to grounding rod or common tower ground.</li>
  <li>Use lightning arrestor between 915 MHz antenna and N bulkhead, crimp on grounding cable with enough slack.</li>
  <li>Use M10 port and M10 cable glands for case grounding wire, connect internal shucked ETH-SP-G2 and case grounding lug with one wire and leave enough of a tail to connect.</li>
  <li>Use split bolts to connect all grounding lines.</li>
</ul>

<h3 id="power-consumption">Power consumption</h3>

<p>This assumes running off 12v barrel connector:
Running idle: 4.3 Watts
TX: 4.8 Watts
Boot-up: 8 Watts<br />
ssh and meshtasticd: 6.x Watts</p>

<p>Still need to do POE power check as well</p>

<h3 id="bom">BOM</h3>

<table>
  <thead>
    <tr>
      <th>Item</th>
      <th>Description</th>
      <th>Min Cost</th>
      <th>Qty</th>
      <th>UOM</th>
      <th>Optional</th>
      <th>Vendor</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Nebra</td>
      <td>Enclosure, antennas, etc</td>
      <td>$40 - $60</td>
      <td>1</td>
      <td>Ea</td>
      <td>No</td>
      <td><a href="https://www.ebay.com">eBay</a></td>
    </tr>
    <tr>
      <td>Pi Hat</td>
      <td>NebraHat</td>
      <td>$30 - $60</td>
      <td>1</td>
      <td>Ea</td>
      <td>No</td>
      <td><a href="https://discord.com">Discord</a></td>
    </tr>
    <tr>
      <td>Stacking headers</td>
      <td>Allowing more than one hat</td>
      <td>$10.00</td>
      <td>1</td>
      <td>Lot</td>
      <td>Yes</td>
      <td><a href="https://www.amazon.com/dp/B084Q4W1PW">Amazon</a></td>
    </tr>
    <tr>
      <td>Grounding wire</td>
      <td>12 AWG solid, THW PVC</td>
      <td>$15.00</td>
      <td>1</td>
      <td>Spool</td>
      <td>No</td>
      <td><a href="https://www.amazon.com/dp/B07M94L2F8">Amazon</a></td>
    </tr>
    <tr>
      <td>Grounding lug</td>
      <td>Works for alum/copper</td>
      <td>$2.28</td>
      <td>1</td>
      <td>2 pack</td>
      <td>No</td>
      <td><a href="https://www.homedepot.com/p/Commercial-Electric-14-AWG-to-2-AWG-Dual-Rated-Mechanical-Lug-with-1-Conductor-and-1-Hole-Mount-2-Pack-G99002/310741850">Home Depot</a></td>
    </tr>
    <tr>
      <td>Split Bolt</td>
      <td>Copper split bolt</td>
      <td>$5.23</td>
      <td>1</td>
      <td>2 pack</td>
      <td>No</td>
      <td><a href="https://www.homedepot.com/p/Commercial-Electric-10-AWG-to-8-AWG-Copper-Split-Bolt-2-Pack-GOEC-15/310741770">Home Depot</a></td>
    </tr>
    <tr>
      <td>Surge protector</td>
      <td>Gas discharge tubes</td>
      <td>$12.50</td>
      <td>2</td>
      <td>Ea</td>
      <td>No</td>
      <td><a href="https://store.ui.com/us/en/category/accessories-poe-power/collections/pro-store-poe-and-power-surge-protection-outdoor/products/ethernet-surge-protector">UBTN</a></td>
    </tr>
    <tr>
      <td>N-Male to N-Male Adapter</td>
      <td>For aftermarket antenna</td>
      <td>$10.00</td>
      <td>1</td>
      <td>2 pack</td>
      <td>Yes</td>
      <td><a href="https://www.amazon.com/dp/B07ZZ1MTC5">Amazon</a></td>
    </tr>
    <tr>
      <td>Antenna</td>
      <td>Antenna upgrade</td>
      <td>$50.00</td>
      <td>1</td>
      <td>Ea</td>
      <td>Yes</td>
      <td><a href="https://www.ebay.com">eBay</a></td>
    </tr>
    <tr>
      <td>u-Green SD Reader</td>
      <td>Flashing eMMC key</td>
      <td>$8.00</td>
      <td>1</td>
      <td>Ea</td>
      <td>Yes</td>
      <td><a href="https://www.amazon.com/dp/B0779V61XB">Amazon</a></td>
    </tr>
    <tr>
      <td>Waterproof Vent Plug</td>
      <td>Allows air and moisture out</td>
      <td>$3.00</td>
      <td>1</td>
      <td>Ea</td>
      <td>Yes</td>
      <td><a href="https://www.aliexpress.us/item/3256806226534115.html">AliExpress</a></td>
    </tr>
    <tr>
      <td>WiFi</td>
      <td>Included Wifi is terrible</td>
      <td>$6.00</td>
      <td>1</td>
      <td>Ea</td>
      <td>Yes</td>
      <td><a href="https://www.aliexpress.us/item/3256807867209673.html">AliExpress</a></td>
    </tr>
  </tbody>
</table>

<p>Basic build can run as low as $100 if good priced Nebra, basic 1W hat and 2 surge protectors. 
Or up to $200 if needing to purchase a lot of ancillary or high end components.</p>

<h3 id="struts">Struts</h3>

<p>If this is going up on a tower, you want to move 1-3 wavelengths out from the tower. It helps with your SWR, reduces tower shadow, etc. Wavelength for 915mhz is 13 inch. But the strut acts like a lever. Further out from the tower, more force is put on the tower from wind load. Another consideration is that you’re adding 5-6 inches of the Nebra case, from the edge of the case to the center of the furthest antenna port, which helps. You can deduct that from the total length you want.</p>

<p>For the pipe, I used Southland Black Iron Pipe from Lowes. I used 1.25 inch pipe, but honestly even 0.75 or 1 inch is fine. I cut into two foot sections. 10ft is more economical, but 6 foot sections fit in the car more easily. It should be slightly more than 1/8th inch thick walls.</p>

<p><img src="/images/posts/nebra/pipe.jpg" alt="pipe cutting" /></p>

<p>For the end pieces, we went with two 9 inch sections of 1/8th angle steel. For the tower end, weld on two U bolts. For the Nebra end, you need to drill two holes. I recommend 9/32 drill bit. You want slightly oversized holes. The holes need to be 108mm apart, edge to edge. 10mm from the outside edge. Easiest way is to photocopy the nebra case on a copier and tape that to one of the angle sections.</p>

<p><img src="/images/posts/nebra/strut-template.jpg" alt="Paper Template" /></p>

<p>Once I got a clean drill, I used a paint marker pen to put the dot on every future piece.</p>

<p><img src="/images/posts/nebra/paint-template.jpg" alt="Paint Template" /></p>

<p>You don’t need to drill the tower end, but you can re-use bad drilled sections for the tower end. Just mark appropriately. I did 100% test fits after drilling.</p>

<p><img src="/images/posts/nebra/test-fit.jpg" alt="Test fit" /></p>

<p>Once you have the holes drilled, weld the pipe to the inside center of both pieces of angle. Spray with cold galvanizing. When putting on the tower, use blue loctite on the nuts. I use two nuts per thread.</p>

<p>We went through a couple iterations increasing strength, decreasing complexity and reducing costs. If you don’t have a metal shop and welder, I recommend reaching out to <a href="https://www.ebay.com/str/2way4u2016">this eBay shop</a> , which has some of our older designs for sale.</p>

<p>You’ll need the following items</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Item</th>
      <th style="text-align: left">Description</th>
      <th style="text-align: left">Length</th>
      <th style="text-align: right">Cost</th>
      <th style="text-align: right">Qty</th>
      <th style="text-align: left">UOM</th>
      <th style="text-align: left">Vendor</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Angle, Mild Steel</td>
      <td style="text-align: left">1.5” x 1.5” x 1/8”</td>
      <td style="text-align: left">9 inch</td>
      <td style="text-align: right">$2</td>
      <td style="text-align: right">2</td>
      <td style="text-align: left">Ea</td>
      <td style="text-align: left"><a href="https://www.ebay.com/str/2twentytwosteeldesigns">eBay</a></td>
    </tr>
    <tr>
      <td style="text-align: left">Pipe, Mild Steel</td>
      <td style="text-align: left">1/8” wall thickness</td>
      <td style="text-align: left">10–30 inch</td>
      <td style="text-align: right">$6–16</td>
      <td style="text-align: right">1</td>
      <td style="text-align: left">Ea</td>
      <td style="text-align: left"><a href="https://www.ebay.com/str/2twentytwosteeldesigns">eBay</a></td>
    </tr>
    <tr>
      <td style="text-align: left">U-Bolt or Muffler Clamp, Mild Steel</td>
      <td style="text-align: left">Various sizes</td>
      <td style="text-align: left">—</td>
      <td style="text-align: right">$4–6</td>
      <td style="text-align: right">2</td>
      <td style="text-align: left">Lot</td>
      <td style="text-align: left"><a href="https://www.ebay.com">eBay</a></td>
    </tr>
    <tr>
      <td style="text-align: left">Spray Galvanizing</td>
      <td style="text-align: left">20 oz can</td>
      <td style="text-align: left">—</td>
      <td style="text-align: right">$10.00</td>
      <td style="text-align: right">1</td>
      <td style="text-align: left">Can</td>
      <td style="text-align: left"><a href="https://www.amazon.com/dp/B00106H68W/?th=1">Amazon</a></td>
    </tr>
  </tbody>
</table>

<p>Most towers are 1.5in tubing.</p>

<h3 id="deployment-lessons-learned">Deployment Lessons Learned</h3>

<ul>
  <li>Ground test your node on site.</li>
  <li>Spares for literally everything. Anything you will NEED, have at least one or two spares.</li>
  <li>You will lose nuts, bolts and U-bolts. Have a baggie of the tower bits for your installer. Stuff will be dropped. Have extra zip ties.</li>
  <li>Test your cables, test your POE injector</li>
  <li>Have lots of different grounding options</li>
  <li>Have a spare fully operational node when doing a tower install. Troubleshooting is time intensive compared to direct swap</li>
  <li>Ground test your rig for at least a week</li>
  <li>Pre-program a WiFi link for the location. In a pinch, set your phone HotSpot to that WiFi network or have your laptop be able to form an Access Point</li>
  <li>Have the tailscale app on your phone. It near instantly tells you when node is online. Plus you can switch from WiFi to cell to verify outside access</li>
  <li>Copy the private and admin keys in advanced so you can remotely reprogram your node via another node</li>
</ul>

<p>Consider tapping your N antenna ports with a <a href="https://www.amazon.com/dp/B07NZP1CXM">5/8”-24 UNEF RH tap</a>. The threads are rough by default. Screw in the tap by hand as much as possible. Only switch to the wrench once you hit resistance and have the tap well seated. Ideally you shouldn’t need the wrench at all. Some cutting fluid, machine oil or lube on the tap would be a good idea but don’t need a lot. If you don’t have any, skip.</p>

<h3 id="the-power-of-the-sun">The Power of the Sun!</h3>

<p>If you got your Nebra and don’t want to muck around with configuring a Pi or 12VDC/POE, you can just use the enclosure for a solar node.</p>

<p><img src="/images/posts/nebra/assembled.png" alt="Assembled Unit" /></p>

<p>For solar, strip the case and mount a WisBlock. A printed <a href="https://www.printables.com/model/893147-meshtastic-Nebra-ip67-mounting-plate">mounting plate</a> works; PETG is ok but ASA is best. Use a 3000 mAh bag battery or even a single 18650. Short IPEX cables on the bulkheads minimize loss, you only have 0.15W TX to play with, but can reuse included bulkheads to save money. A <a href="https://www.printables.com/model/1264626-rak-ipex-pigtail-bracket">pigtail bracket</a> holds connectors in place. Use #2 screws. 2.4 GHz WiFi antenna works fine for Bluetooth on the Wisblock</p>

<p>BE VERY CAREFUL WITH THE BATTERY WIRING, YOU CAN EASILY FRY THE BOARD IF THE WIRING IS REVERSED. Verify the + marking on the battery, and the + next to the battery connector. Do not rely on wire color.</p>

<p><img src="/images/posts/nebra/mount.png" alt="Mount" /></p>

<p>I use a <a href="https://www.amazon.com/dp/B0BVT4J3FF">mounting bracket</a> to connect the solar panel to the miner, along with metal hose clamp. The solar panel I had used a 1/4 in course threaded nut. The hose clamp are probably the most secure way to fasten the two together. One honestly is fine, and trim slack.</p>

<p>I wrap the solar panel line connectors with silicon tape, plus the bulkheads. As well as spiral wrap for air hoses, to hopefully keep wildlife from eating the external power cable.</p>

<p><img src="/images/posts/nebra/deployed.png" alt="Deployed Unit" /></p>]]></content><author><name>Casper</name></author><category term="meshtastic" /><category term="LoRA" /><category term="crypto" /><summary type="html"><![CDATA[Low cost high power LoRA Mesh networking]]></summary></entry><entry><title type="html">Infor Visual - Bookings Reports</title><link href="/Visual-Bookings-Reports/" rel="alternate" type="text/html" title="Infor Visual - Bookings Reports" /><published>2025-05-11T00:00:00+00:00</published><updated>2025-05-11T20:00:00+00:00</updated><id>/Visual-Bookings-Reports</id><content type="html" xml:base="/Visual-Bookings-Reports/"><![CDATA[<h2 id="visual-bookings">Visual Bookings</h2>

<p>Our sales folks want to keep track of both new orders and changes. We want to “book” the changes to the day they’re made.</p>

<p>Bookings reports themselves are half easy and half a pain in the neck. Checking for new customer orders is pretty easy. You can just use the CREATE_DATE from customer_order table.</p>

<p>But if you want to track changes to existing orders, it gets a bit more tricky but there is a handy auditing feature built into Infor Visual.</p>

<h2 id="auditing">Auditing</h2>

<p>Fire up Visual, go to Admin menu, click on Audit Maintenance</p>

<p><img src="/images/posts/booking/bookings03.png" alt="Audit Maint" /></p>

<p>Right click on any entry, click on “Configure Auditing”</p>

<p><img src="/images/posts/booking/bookings02.png" alt="Audit Maint" /></p>

<p>Go to CUST_ORDER_LINE, turn on UNIT_PRICE and ORDER_QTY. Plus anything else you want to monitor.</p>

<p><img src="/images/posts/booking/bookings01.png" alt="Audit Maint" /></p>

<p>All of the data will be recorded to HISTORY_DATA table. Be careful how much is recorded, less is more. And purge the records as needed. Since both are on the history table and neatly timestamped there, I stuck to using that table for new customer orders as well as the changes.</p>

<p>INSERT means new customer order.
UPDATE means changed customer order.</p>

<h2 id="code-explanation">Code explanation</h2>

<p>The problem with the auditing table is that the table name, column name, etc are all separate rows. Piecing them back together into single rows can be a bit of a pain. The primary key for customer orders is pretty easy with just a tilde (~) between the customer order number and line number. You just need to do a LEFT and RIGHT to work with it. Other tables being audited can have a bit more unpleasant primary keys to parse.</p>

<p>I left the parameters as SSRS code, but fixed date parameters are available. Just be sure to grab both.</p>

<p>To assemble the data, I used a CTE, which creates a virtual table ChangeData. This was to consolidate rows that are changes to customer orders. The not great part is needing to use MAX, which I was worried would skew the data. So far it hasn’t and the numbers look accurate. Then a select statement grabs the changes, and it gets added via UNION to the new orders.</p>

<h2 id="code">Code</h2>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">USE</span> <span class="n">PFI10</span><span class="p">;</span>


<span class="c1">-- ==============================================</span>
<span class="c1">-- 1. Changes to ORDER_QTY and/or UNIT_PRICE</span>
<span class="c1">-- ==============================================</span>
<span class="k">WITH</span> <span class="n">ChangeData</span> <span class="k">AS</span> <span class="p">(</span>
	<span class="k">SELECT</span>
		<span class="k">CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">CREATE_DATE</span> <span class="k">AS</span> <span class="nb">DATE</span><span class="p">)</span> <span class="k">AS</span> <span class="n">ChangeDate</span><span class="p">,</span>
		<span class="k">LEFT</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">,</span> <span class="n">CHARINDEX</span><span class="p">(</span><span class="s1">'~'</span><span class="p">,</span> <span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="k">AS</span> <span class="n">CustOrderID</span><span class="p">,</span>
		<span class="n">TRY_CAST</span><span class="p">(</span><span class="k">RIGHT</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">,</span> <span class="n">LEN</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">)</span> <span class="o">-</span> <span class="n">CHARINDEX</span><span class="p">(</span><span class="s1">'~'</span><span class="p">,</span> <span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">))</span> <span class="k">AS</span> <span class="nb">INT</span><span class="p">)</span> <span class="k">AS</span> <span class="n">Line_No</span><span class="p">,</span>
		<span class="k">MAX</span><span class="p">(</span><span class="k">CASE</span> <span class="k">WHEN</span> <span class="n">hd</span><span class="p">.</span><span class="n">COL_NAME</span> <span class="o">=</span> <span class="s1">'ORDER_QTY'</span> <span class="k">THEN</span> <span class="n">TRY_CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">OLD_VALUE</span> <span class="k">AS</span> <span class="nb">FLOAT</span><span class="p">)</span> <span class="k">END</span><span class="p">)</span> <span class="k">AS</span> <span class="n">OldQty</span><span class="p">,</span>
		<span class="k">MAX</span><span class="p">(</span><span class="k">CASE</span> <span class="k">WHEN</span> <span class="n">hd</span><span class="p">.</span><span class="n">COL_NAME</span> <span class="o">=</span> <span class="s1">'ORDER_QTY'</span> <span class="k">THEN</span> <span class="n">TRY_CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">NEW_VALUE</span> <span class="k">AS</span> <span class="nb">FLOAT</span><span class="p">)</span> <span class="k">END</span><span class="p">)</span> <span class="k">AS</span> <span class="n">NewQty</span><span class="p">,</span>
		<span class="k">MAX</span><span class="p">(</span><span class="k">CASE</span> <span class="k">WHEN</span> <span class="n">hd</span><span class="p">.</span><span class="n">COL_NAME</span> <span class="o">=</span> <span class="s1">'UNIT_PRICE'</span> <span class="k">THEN</span> <span class="n">TRY_CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">OLD_VALUE</span> <span class="k">AS</span> <span class="nb">FLOAT</span><span class="p">)</span> <span class="k">END</span><span class="p">)</span> <span class="k">AS</span> <span class="n">OldPrice</span><span class="p">,</span>
		<span class="k">MAX</span><span class="p">(</span><span class="k">CASE</span> <span class="k">WHEN</span> <span class="n">hd</span><span class="p">.</span><span class="n">COL_NAME</span> <span class="o">=</span> <span class="s1">'UNIT_PRICE'</span> <span class="k">THEN</span> <span class="n">TRY_CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">NEW_VALUE</span> <span class="k">AS</span> <span class="nb">FLOAT</span><span class="p">)</span> <span class="k">END</span><span class="p">)</span> <span class="k">AS</span> <span class="n">NewPrice</span>
	<span class="k">FROM</span> <span class="n">HISTORY_DATA</span> <span class="n">hd</span>
	<span class="k">WHERE</span> 
		<span class="n">hd</span><span class="p">.</span><span class="n">TBL_NAME</span> <span class="o">=</span> <span class="s1">'CUST_ORDER_LINE'</span>
		<span class="k">AND</span> <span class="n">hd</span><span class="p">.</span><span class="n">ACTION</span> <span class="o">=</span> <span class="s1">'UPDATE'</span>
		<span class="k">AND</span> <span class="n">hd</span><span class="p">.</span><span class="n">COL_NAME</span> <span class="k">IN</span> <span class="p">(</span><span class="s1">'ORDER_QTY'</span><span class="p">,</span> <span class="s1">'UNIT_PRICE'</span><span class="p">)</span>
		<span class="c1">--AND CAST(hd.CREATE_DATE AS DATE) &gt;= '2025-05-01'</span>
		<span class="k">AND</span> <span class="p">(</span> 
			<span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">CREATE_DATE</span> <span class="k">AS</span> <span class="nb">DATE</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="o">@</span><span class="n">StartDate</span><span class="p">)</span> <span class="k">AND</span> <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">CREATE_DATE</span> <span class="k">AS</span> <span class="nb">DATE</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="o">@</span><span class="n">EndDate</span><span class="p">)</span>
			<span class="p">)</span>
	<span class="k">GROUP</span> <span class="k">BY</span> 
		<span class="k">CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">CREATE_DATE</span> <span class="k">AS</span> <span class="nb">DATE</span><span class="p">),</span>
		<span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span>
<span class="p">)</span>
<span class="k">SELECT</span>
	<span class="n">cd</span><span class="p">.</span><span class="n">ChangeDate</span> <span class="k">AS</span> <span class="n">CREATE_DATE</span><span class="p">,</span>
	<span class="s1">'Change - Qty or Price'</span> <span class="k">AS</span> <span class="n">BookingType</span><span class="p">,</span>
	<span class="n">col</span><span class="p">.</span><span class="n">PART_ID</span><span class="p">,</span>
	<span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
	<span class="n">col</span><span class="p">.</span><span class="n">LINE_STATUS</span><span class="p">,</span>
	<span class="n">col</span><span class="p">.</span><span class="n">DESIRED_SHIP_DATE</span><span class="p">,</span>
	<span class="n">cd</span><span class="p">.</span><span class="n">CustOrderID</span><span class="p">,</span>
	<span class="n">cd</span><span class="p">.</span><span class="n">Line_No</span><span class="p">,</span>
	<span class="k">ISNULL</span><span class="p">(</span><span class="n">cd</span><span class="p">.</span><span class="n">NewQty</span> <span class="o">-</span> <span class="n">cd</span><span class="p">.</span><span class="n">OldQty</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">AS</span> <span class="n">Qty</span><span class="p">,</span>
	<span class="k">ISNULL</span><span class="p">(</span><span class="n">cd</span><span class="p">.</span><span class="n">NewPrice</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">UNIT_PRICE</span><span class="p">)</span> <span class="k">AS</span> <span class="n">UNIT_PRICE</span><span class="p">,</span>
	<span class="c1">-- Value Change</span>
	<span class="p">(</span><span class="k">ISNULL</span><span class="p">(</span><span class="n">cd</span><span class="p">.</span><span class="n">NewQty</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">ORDER_QTY</span><span class="p">)</span> <span class="o">*</span> <span class="k">ISNULL</span><span class="p">(</span><span class="n">cd</span><span class="p">.</span><span class="n">NewPrice</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">UNIT_PRICE</span><span class="p">))</span> 
	  <span class="o">-</span> <span class="p">(</span><span class="k">ISNULL</span><span class="p">(</span><span class="n">cd</span><span class="p">.</span><span class="n">OldQty</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">ORDER_QTY</span><span class="p">)</span> <span class="o">*</span> <span class="k">ISNULL</span><span class="p">(</span><span class="n">cd</span><span class="p">.</span><span class="n">OldPrice</span><span class="p">,</span> <span class="n">col</span><span class="p">.</span><span class="n">UNIT_PRICE</span><span class="p">))</span> <span class="k">AS</span> <span class="n">TotalAmt</span>
<span class="k">FROM</span> <span class="n">ChangeData</span> <span class="n">cd</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">CUST_ORDER_LINE</span> <span class="n">col</span>
	<span class="k">ON</span> <span class="n">col</span><span class="p">.</span><span class="n">CUST_ORDER_ID</span> <span class="o">=</span> <span class="n">cd</span><span class="p">.</span><span class="n">CustOrderID</span> <span class="k">AND</span> <span class="n">col</span><span class="p">.</span><span class="n">LINE_NO</span> <span class="o">=</span> <span class="n">cd</span><span class="p">.</span><span class="n">Line_No</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">CUSTOMER_ORDER</span> <span class="k">as</span> <span class="n">co</span>
	<span class="k">ON</span> <span class="n">col</span><span class="p">.</span><span class="n">cust_order_id</span> <span class="o">=</span> <span class="n">co</span><span class="p">.</span><span class="n">id</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">CUSTOMER</span> <span class="k">as</span> <span class="k">c</span>
	<span class="k">ON</span> <span class="n">co</span><span class="p">.</span><span class="n">CUSTOMER_ID</span> <span class="o">=</span> <span class="k">c</span><span class="p">.</span><span class="n">ID</span>
<span class="k">WHERE</span> 
	<span class="c1">-- Only include records with an actual change</span>
	<span class="p">(</span><span class="k">ISNULL</span><span class="p">(</span><span class="n">cd</span><span class="p">.</span><span class="n">NewQty</span> <span class="o">-</span> <span class="n">cd</span><span class="p">.</span><span class="n">OldQty</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&lt;&gt;</span> <span class="mi">0</span> <span class="k">OR</span> <span class="k">ISNULL</span><span class="p">(</span><span class="n">cd</span><span class="p">.</span><span class="n">NewPrice</span> <span class="o">-</span> <span class="n">cd</span><span class="p">.</span><span class="n">OldPrice</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&lt;&gt;</span> <span class="mi">0</span><span class="p">)</span>
<span class="k">UNION</span> <span class="k">ALL</span>
<span class="c1">-- ======================================</span>
<span class="c1">-- 2. New Bookings (INSERT on ORDER_QTY)</span>
<span class="c1">-- ======================================</span>
<span class="k">SELECT</span>  
	<span class="k">CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">CREATE_DATE</span> <span class="k">AS</span> <span class="nb">DATE</span><span class="p">)</span> <span class="k">AS</span> <span class="n">CREATE_DATE</span><span class="p">,</span>
	<span class="s1">'New Order'</span> <span class="k">AS</span> <span class="n">BookingType</span><span class="p">,</span>
	<span class="n">col</span><span class="p">.</span><span class="n">PART_ID</span><span class="p">,</span>
	<span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
	<span class="n">col</span><span class="p">.</span><span class="n">LINE_STATUS</span><span class="p">,</span>
	<span class="n">col</span><span class="p">.</span><span class="n">DESIRED_SHIP_DATE</span><span class="p">,</span>
	<span class="k">LEFT</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">,</span> <span class="n">CHARINDEX</span><span class="p">(</span><span class="s1">'~'</span><span class="p">,</span> <span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="k">AS</span> <span class="n">CustOrderID</span><span class="p">,</span>
	<span class="n">TRY_CAST</span><span class="p">(</span><span class="k">RIGHT</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">,</span> <span class="n">LEN</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">)</span> <span class="o">-</span> <span class="n">CHARINDEX</span><span class="p">(</span><span class="s1">'~'</span><span class="p">,</span> <span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">))</span> <span class="k">AS</span> <span class="nb">INT</span><span class="p">)</span> <span class="k">AS</span> <span class="n">Line_No</span><span class="p">,</span>
	<span class="n">TRY_CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">NEW_VALUE</span> <span class="k">AS</span> <span class="nb">FLOAT</span><span class="p">)</span> <span class="k">AS</span> <span class="n">Qty</span><span class="p">,</span>
	<span class="n">col</span><span class="p">.</span><span class="n">UNIT_PRICE</span><span class="p">,</span>
	<span class="n">TRY_CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">NEW_VALUE</span> <span class="k">AS</span> <span class="nb">FLOAT</span><span class="p">)</span> <span class="o">*</span> <span class="n">col</span><span class="p">.</span><span class="n">UNIT_PRICE</span> <span class="k">AS</span> <span class="n">TotalAmt</span>
<span class="k">FROM</span> <span class="n">HISTORY_DATA</span> <span class="k">AS</span> <span class="n">hd</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">CUST_ORDER_LINE</span> <span class="k">AS</span> <span class="n">col</span>
	<span class="k">ON</span> <span class="n">col</span><span class="p">.</span><span class="n">CUST_ORDER_ID</span> <span class="o">=</span> <span class="k">LEFT</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">,</span> <span class="n">CHARINDEX</span><span class="p">(</span><span class="s1">'~'</span><span class="p">,</span> <span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> 
	<span class="k">AND</span> <span class="n">col</span><span class="p">.</span><span class="n">LINE_NO</span> <span class="o">=</span> <span class="n">TRY_CAST</span><span class="p">(</span><span class="k">RIGHT</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">,</span> <span class="n">LEN</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">)</span> <span class="o">-</span> <span class="n">CHARINDEX</span><span class="p">(</span><span class="s1">'~'</span><span class="p">,</span> <span class="n">hd</span><span class="p">.</span><span class="n">PRIMARY_KEY</span><span class="p">))</span> <span class="k">AS</span> <span class="nb">INT</span><span class="p">)</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">CUSTOMER_ORDER</span> <span class="k">as</span> <span class="n">co</span>
	<span class="k">ON</span> <span class="n">col</span><span class="p">.</span><span class="n">cust_order_id</span> <span class="o">=</span> <span class="n">co</span><span class="p">.</span><span class="n">id</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">CUSTOMER</span> <span class="k">as</span> <span class="k">c</span>
	<span class="k">ON</span> <span class="n">co</span><span class="p">.</span><span class="n">CUSTOMER_ID</span> <span class="o">=</span> <span class="k">c</span><span class="p">.</span><span class="n">ID</span>
<span class="k">WHERE</span> 
	<span class="n">hd</span><span class="p">.</span><span class="n">TBL_NAME</span> <span class="o">=</span> <span class="s1">'CUST_ORDER_LINE'</span>
	<span class="k">AND</span> <span class="n">hd</span><span class="p">.</span><span class="n">ACTION</span> <span class="o">=</span> <span class="s1">'INSERT'</span>
	<span class="k">AND</span> <span class="n">hd</span><span class="p">.</span><span class="n">COL_NAME</span> <span class="o">=</span> <span class="s1">'ORDER_QTY'</span>
	<span class="c1">--AND CAST(hd.CREATE_DATE AS DATE) &gt;= '2025-05-01'</span>
	<span class="k">AND</span> <span class="p">(</span> 
			<span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">CREATE_DATE</span> <span class="k">AS</span> <span class="nb">DATE</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="o">@</span><span class="n">StartDate</span><span class="p">)</span> <span class="k">AND</span> <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="n">hd</span><span class="p">.</span><span class="n">CREATE_DATE</span> <span class="k">AS</span> <span class="nb">DATE</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="o">@</span><span class="n">EndDate</span><span class="p">)</span>
			<span class="p">)</span>

<span class="k">ORDER</span> <span class="k">BY</span>
	<span class="k">c</span><span class="p">.</span><span class="n">name</span> <span class="k">asc</span><span class="p">,</span>
	<span class="n">BookingType</span> <span class="k">DESC</span><span class="p">,</span>
	<span class="n">CREATE_DATE</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="final-thoughts">Final thoughts</h2>

<p>So far this seems to be working fine on every example we’ve checked. If we need to go overboard on this, it’ll probably be setting up another table and a database trigger to keep track of CO changes outside of the Visual audit mechanism.</p>

<h2 id="attribution">Attribution</h2>

<p>If you wish to use this guide internally, just maintain attribution/copyright with a link to this article. 
Original link: https://casper.im/Visual-Queries/</p>]]></content><author><name>Casper</name></author><category term="Infor" /><category term="Infor Visual" /><category term="SQL" /><summary type="html"><![CDATA[Keeping track of new and changed orders]]></summary></entry><entry><title type="html">Infor Visual - Recursive Traceability across Multiple Part Operations</title><link href="/Recursive-Traceability/" rel="alternate" type="text/html" title="Infor Visual - Recursive Traceability across Multiple Part Operations" /><published>2025-05-06T00:00:00+00:00</published><updated>2025-05-06T20:00:00+00:00</updated><id>/Recursive-Traceability</id><content type="html" xml:base="/Recursive-Traceability/"><![CDATA[<h2 id="whats-the-problem">What’s the problem?</h2>

<p>Traceability can be a pain. With Infor Visual, you have to loop through the inventory transaction table, trace inventory transactions and trace table. Multiple times.  We wanted to be able to easily chain finished good trace numbers back to the raw material trace number.</p>

<p>Basically, we need an unbroken chain of custody from the finished part sold to the end customer all the way back to the original mine.</p>

<p>Visual doesn’t make that easy. It doesn’t store trace numbers in any convenient fashion.</p>

<h2 id="things-get-unweldy">Things get unweldy</h2>

<p>You can manually code it if you know how many layers your part has. But in my case, I had a part with three stages of production but we were adding a fourth, with more possible. That racked up about 30 joins to brute force the solution. To make it more fun, we store the heat treat info with the raw material trace number as APROPERTY_1.</p>

<p>SQL doesn’t have built in recursion. But common table expression are a way to bypass that limitation. CTEs create a virtual tables with records and columns once executed. It’s not a temp table that is somewhat persistant, it’s created on runtime, not shared and gone once the query is finished. Temp tables can get goofy with reports, especially if it’s rerun or multiple people may use the same report at the same time.</p>

<h2 id="tackling-the-problem">Tackling the problem</h2>

<p>We first do a manual query to set a root trace ID. For our certification, we work off pack lists because shipping department generates them. It’s possible to work off work orders, but requires more code. I use SSRS, so left @WhatPackList in as a variable, but you can hardcode in a pack list as well. Fortunately it links to the inventory transaction table via shipper line neatly.</p>

<p>I store the path in TracePath, useful for testing but disabled in production. But I created another report so users can easily see the path. This however had a real issue with recursive loops that took a bit to noodle around. I figured it out through trial and error, you wanted to avoid any path that would contain the second trace table ID.</p>

<p>Each step requires going through inventory transaction table twice.</p>

<p>Once you have the final information, you can grab all the extra tables for ancillary info by linking to the virtual table.</p>

<p>You can also link in the work order as well as pack list if you have multiple raw material ID for the packlist. If you have multiple raw material ID’s per work order and lot number, you have bigger problems. But you can probably work with this code to get a list.</p>

<h2 id="code">Code</h2>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code>

<span class="c1">-- Recursive Trace </span>
<span class="c1">--</span>
<span class="c1">--  Written by Chris Casper</span>
<span class="c1">--  v1 - 2025.05.06</span>
<span class="c1">--</span>
<span class="c1">--</span>
<span class="c1">--</span>

<span class="k">WITH</span> <span class="n">TraceCTE</span> <span class="k">AS</span> <span class="p">(</span>
    <span class="c1">-- Anchor: Start from finished good trace</span>
    <span class="k">SELECT</span> 
        <span class="n">t</span><span class="p">.</span><span class="n">ID</span> <span class="k">AS</span> <span class="n">TraceID</span><span class="p">,</span>
        <span class="n">t</span><span class="p">.</span><span class="n">ID</span> <span class="k">AS</span> <span class="n">RootTraceID</span><span class="p">,</span>
		<span class="n">t</span><span class="p">.</span><span class="n">APROPERTY_1</span> <span class="k">as</span> <span class="n">HeatTreatNum</span><span class="p">,</span>
        <span class="k">CAST</span><span class="p">(</span><span class="n">t</span><span class="p">.</span><span class="n">ID</span> <span class="k">AS</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">))</span> <span class="k">AS</span> <span class="n">TracePath</span>
    <span class="k">FROM</span> 
        <span class="n">shipper</span> <span class="n">s</span>
        <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">shipper_line</span> <span class="n">sl</span> <span class="k">ON</span> <span class="n">s</span><span class="p">.</span><span class="n">packlist_id</span> <span class="o">=</span> <span class="n">sl</span><span class="p">.</span><span class="n">packlist_id</span>
        <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">TRACE_INV_TRANS</span> <span class="n">ti</span> <span class="k">ON</span> <span class="n">sl</span><span class="p">.</span><span class="n">transaction_id</span> <span class="o">=</span> <span class="n">ti</span><span class="p">.</span><span class="n">transaction_id</span>
        <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">TRACE</span> <span class="n">t</span> <span class="k">ON</span> <span class="n">ti</span><span class="p">.</span><span class="n">trace_id</span> <span class="o">=</span> <span class="n">t</span><span class="p">.</span><span class="n">id</span>
    <span class="k">WHERE</span> 
        <span class="c1">--s.packlist_id = 'PL012345'</span>
	<span class="n">s</span><span class="p">.</span><span class="n">packlist_id</span> <span class="o">=</span> <span class="o">@</span><span class="n">WhatPackList</span>

    <span class="k">UNION</span> <span class="k">ALL</span>

    <span class="c1">-- Recursive: trace backwards but prevent revisiting a trace already in the path</span>
    <span class="k">SELECT</span> 
        <span class="n">t2</span><span class="p">.</span><span class="n">ID</span> <span class="k">AS</span> <span class="n">TraceID</span><span class="p">,</span>
        <span class="n">tc</span><span class="p">.</span><span class="n">RootTraceID</span><span class="p">,</span>
		<span class="n">t2</span><span class="p">.</span><span class="n">APROPERTY_1</span> <span class="k">as</span> <span class="n">HeatTreatNum</span><span class="p">,</span>
        <span class="k">CAST</span><span class="p">(</span><span class="n">tc</span><span class="p">.</span><span class="n">TracePath</span> <span class="o">+</span> <span class="s1">' &lt;- '</span> <span class="o">+</span> <span class="k">CAST</span><span class="p">(</span><span class="n">t2</span><span class="p">.</span><span class="n">ID</span> <span class="k">AS</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">))</span> <span class="k">AS</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">))</span> <span class="k">AS</span> <span class="n">TracePath</span>
    <span class="k">FROM</span> 
        <span class="n">TraceCTE</span> <span class="n">tc</span>
        <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">TRACE_INV_TRANS</span> <span class="n">ti1</span> <span class="k">ON</span> <span class="n">tc</span><span class="p">.</span><span class="n">TraceID</span> <span class="o">=</span> <span class="n">ti1</span><span class="p">.</span><span class="n">TRACE_ID</span>
        <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">INVENTORY_TRANS</span> <span class="n">it1</span> <span class="k">ON</span> <span class="n">ti1</span><span class="p">.</span><span class="n">TRANSACTION_ID</span> <span class="o">=</span> <span class="n">it1</span><span class="p">.</span><span class="n">TRANSACTION_ID</span>
        <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">INVENTORY_TRANS</span> <span class="n">it2</span> 
            <span class="k">ON</span> <span class="n">it1</span><span class="p">.</span><span class="n">WORKORDER_BASE_ID</span> <span class="o">=</span> <span class="n">it2</span><span class="p">.</span><span class="n">WORKORDER_BASE_ID</span>
            <span class="k">AND</span> <span class="n">it1</span><span class="p">.</span><span class="n">WORKORDER_LOT_ID</span> <span class="o">=</span> <span class="n">it2</span><span class="p">.</span><span class="n">WORKORDER_LOT_ID</span>
            <span class="k">AND</span> <span class="n">it2</span><span class="p">.</span><span class="k">TYPE</span> <span class="o">=</span> <span class="s1">'O'</span>
            <span class="k">AND</span> <span class="n">it2</span><span class="p">.</span><span class="k">CLASS</span> <span class="o">=</span> <span class="s1">'I'</span>
        <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">TRACE_INV_TRANS</span> <span class="n">ti2</span> <span class="k">ON</span> <span class="n">it2</span><span class="p">.</span><span class="n">TRANSACTION_ID</span> <span class="o">=</span> <span class="n">ti2</span><span class="p">.</span><span class="n">TRANSACTION_ID</span>
        <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">TRACE</span> <span class="n">t2</span> <span class="k">ON</span> <span class="n">ti2</span><span class="p">.</span><span class="n">TRACE_ID</span> <span class="o">=</span> <span class="n">t2</span><span class="p">.</span><span class="n">ID</span>
    <span class="k">WHERE</span>  <span class="c1">-- Removing this will cause recursive loop</span>
        <span class="n">tc</span><span class="p">.</span><span class="n">TracePath</span> <span class="k">NOT</span> <span class="k">LIKE</span> <span class="s1">'%'</span> <span class="o">+</span> <span class="k">CAST</span><span class="p">(</span><span class="n">t2</span><span class="p">.</span><span class="n">ID</span> <span class="k">AS</span> <span class="nb">VARCHAR</span><span class="p">)</span> <span class="o">+</span> <span class="s1">'%'</span>
<span class="p">)</span>
<span class="k">SELECT</span> <span class="k">DISTINCT</span> <span class="n">top</span> <span class="mi">1</span>

	<span class="c1">-- Uncomment for whatever data you need</span>
	
    <span class="c1">--s.packlist_id as packlist_id</span>
	<span class="c1">--, it.PART_ID</span>
    <span class="c1">--, it.WORKORDER_BASE_ID as WORKORDER_BASE_ID</span>
	<span class="n">tc</span><span class="p">.</span><span class="n">HeatTreatNum</span> <span class="k">as</span> <span class="n">LastHeatTreat</span>  <span class="c1">-- remember, we're getting value from CTE, not directly from table </span>
    <span class="c1">--, tc.TraceID AS RelatedTraceID</span>
	<span class="c1">--, it.TRANSACTION_DATE</span>

<span class="k">FROM</span> 
    <span class="n">TraceCTE</span> <span class="n">tc</span>

    <span class="c1">-- Link back to transactions and work orders</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">TRACE_INV_TRANS</span> <span class="n">ti</span> <span class="k">ON</span> <span class="n">tc</span><span class="p">.</span><span class="n">TraceID</span> <span class="o">=</span> <span class="n">ti</span><span class="p">.</span><span class="n">TRACE_ID</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">INVENTORY_TRANS</span> <span class="n">it</span> <span class="k">ON</span> <span class="n">ti</span><span class="p">.</span><span class="n">TRANSACTION_ID</span> <span class="o">=</span> <span class="n">it</span><span class="p">.</span><span class="n">TRANSACTION_ID</span>

    <span class="c1">-- Joins from original query for the misc data</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">shipper_line</span> <span class="n">sl</span> <span class="k">ON</span> <span class="n">ti</span><span class="p">.</span><span class="n">transaction_id</span> <span class="o">=</span> <span class="n">sl</span><span class="p">.</span><span class="n">transaction_id</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">shipper</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">sl</span><span class="p">.</span><span class="n">packlist_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">packlist_id</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">customer_order</span> <span class="n">co</span> <span class="k">ON</span> <span class="n">s</span><span class="p">.</span><span class="n">cust_order_id</span> <span class="o">=</span> <span class="n">co</span><span class="p">.</span><span class="n">id</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">cust_address</span> <span class="n">ca</span> <span class="k">ON</span> <span class="n">co</span><span class="p">.</span><span class="n">customer_id</span> <span class="o">=</span> <span class="n">ca</span><span class="p">.</span><span class="n">customer_id</span> <span class="k">AND</span> <span class="n">co</span><span class="p">.</span><span class="n">SHIP_TO_ADDR_NO</span> <span class="o">=</span> <span class="n">ca</span><span class="p">.</span><span class="n">ADDR_NO</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">SHIPTO_ADDRESS</span> <span class="n">sa</span> <span class="k">ON</span> <span class="n">ca</span><span class="p">.</span><span class="n">SHIPTO_ID</span> <span class="o">=</span> <span class="n">sa</span><span class="p">.</span><span class="n">SHIPTO_ID</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">cust_order_line</span> <span class="n">col</span> <span class="k">ON</span> <span class="n">s</span><span class="p">.</span><span class="n">cust_order_id</span> <span class="o">=</span> <span class="n">col</span><span class="p">.</span><span class="n">cust_order_id</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">customer</span> <span class="k">c</span> <span class="k">ON</span> <span class="n">co</span><span class="p">.</span><span class="n">customer_id</span> <span class="o">=</span> <span class="k">c</span><span class="p">.</span><span class="n">id</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">part</span> <span class="n">p</span> <span class="k">ON</span> <span class="n">col</span><span class="p">.</span><span class="n">part_id</span> <span class="o">=</span> <span class="n">p</span><span class="p">.</span><span class="n">id</span>
    <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">part_site</span> <span class="n">pst</span> <span class="k">ON</span> <span class="n">col</span><span class="p">.</span><span class="n">part_id</span> <span class="o">=</span> <span class="n">pst</span><span class="p">.</span><span class="n">part_id</span>
<span class="c1">--where </span>
	<span class="c1">--it.PART_ID like 'RM-%'</span>
	<span class="c1">--it.WORKORDER_BASE_ID like 'X0123456'</span>
	<span class="c1">--it.WORKORDER_BASE_ID like @WhatWorkOrder</span>
<span class="k">where</span> 
	<span class="c1">--(ti.QTY &gt;= '0') </span>
	<span class="c1">--OR </span>
	<span class="p">(</span><span class="n">tc</span><span class="p">.</span><span class="n">HeatTreatNum</span> <span class="k">is</span> <span class="k">not</span> <span class="k">null</span><span class="p">)</span> 


<span class="k">OPTION</span> <span class="p">(</span><span class="n">MAXRECURSION</span> <span class="mi">100</span><span class="p">);</span>




</code></pre></div></div>]]></content><author><name>Casper</name></author><category term="Infor Visual" /><category term="SQL" /><summary type="html"><![CDATA[Material certifications, definitely an exciting topic]]></summary></entry><entry><title type="html">Nicelabel File Export</title><link href="/Nicelabel-File-Export/" rel="alternate" type="text/html" title="Nicelabel File Export" /><published>2025-04-03T00:00:00+00:00</published><updated>2025-04-03T20:00:00+00:00</updated><id>/Nicelabel-File-Export</id><content type="html" xml:base="/Nicelabel-File-Export/"><![CDATA[<h2 id="nicelabel">Nicelabel</h2>

<p>Nicelabel is a bulk label software package. They have a storage system that uses WebDAV for accessing files from Print Stations, the label making software, etc. It’s handy but I’m not sure why they didn’t go with an SMB share. I’m assuming for access control. Rather than just locking down SMB NTFS permissions and performing file operations via ACL’s within the software, maybe.</p>

<p>The central file storage is controlled by “EPM”, which is their web console.</p>

<p>All and all, pretty decent so far.</p>

<h2 id="problem">Problem</h2>

<p>So a power user that works on the label layouts mucked up a file. I figured it wouldn’t be too hard to recover the file because I am a bit nuts about backups. Two different independent backup systems and hourly backups. But… I had no idea where the files were physically stored.</p>

<p>After looking around, I reached out to support.</p>

<h2 id="support">Support</h2>

<p>Nicelabel’s second and third tier support are excellent. They know their stuff and are super helpful.</p>

<p>They provided the info that the files are stored in the DB, not in flat files. And that the official guidance is to enable version control. But if you haven’t turned that on, you’d need to restore the DB or fire up a virtual clone, restore the DB on that clone and export Nicelabel Designer from the clone. That sounds like work, so I went looking for a better solution.</p>

<h2 id="exporting-files-from-t-sql">Exporting files from T-SQL</h2>

<p>This isn’t uncommon. SSRS does the same thing. But there is a handy powershell command to export all of the files to RDL. I schedule it nightly.</p>

<p>It’s saved my bacon many times, and I always implement it when firing up a new SSRS instance.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install-Module -Name ReportingServicesTools</span><span class="w">
</span><span class="c"># OR</span><span class="w">
</span><span class="c"># Invoke-Expression (Invoke-WebRequest https://raw.githubusercontent.com/Microsoft/ReportingServicesTools/master/Install.ps1)</span><span class="w">


</span><span class="c">#Declare SSRS URI</span><span class="w">
</span><span class="nv">$sourceRsUri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'http://ssrs-server:8080/ReportServer/'</span><span class="w">

</span><span class="c">#Declare Proxy so we dont need to connect with every command</span><span class="w">
</span><span class="nv">$proxy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-RsWebServiceProxy</span><span class="w"> </span><span class="nt">-ReportServerUri</span><span class="w"> </span><span class="nv">$sourceRsUri</span><span class="w">

</span><span class="c">#Output ALL Catalog items to file system</span><span class="w">
</span><span class="n">Out-RsFolderContent</span><span class="w"> </span><span class="nt">-Proxy</span><span class="w"> </span><span class="nv">$proxy</span><span class="w"> </span><span class="nt">-RsFolder</span><span class="w"> </span><span class="nx">/</span><span class="w"> </span><span class="nt">-Destination</span><span class="w"> </span><span class="s1">'C:\Backups\SSRS'</span><span class="w"> </span><span class="nt">-Recurse</span><span class="w"> 
</span></code></pre></div></div>

<p>Originally I tried using BCP to export the files, but it’s extremely picky.</p>

<p>Microsoft ‘solved’ the issue by having FMT file that provides the settings for export. But if you don’t know the settings, it doesn’t go well. I learned a bit more about <a href="https://medium.com/@0xwan/png-structure-for-beginner-8363ce2a9f73">PNG forensics</a> but ultimately gave up because it was taking too long, and I found a new really nice hex editor, <a href="https://imhex.werwolv.net/">ImHex</a>. BCP kept dumping 8 byte header in front of the real data. I could have done a hacky solution to chop those bytes but I wasn’t thrilled.</p>

<p>Nicelabel techs tried to be supportive, but didn’t know the answer either.</p>

<p>I got a hint from a <a href="https://sqlrambling.net/2020/04/04/saving-and-extracting-blob-data-basic-examples/">SQL blog</a> about using OLE object creation. Honestly I should have coded it in powershell, but instead I just wrote the SQL and kick it off with powershell. Works well enough, and I’m scheduling for nightly. I checked all of the file types and they all seem to export cleanly. Someday I might clean it up and rewrite in native powershell.</p>

<p>Nicelabel-File-Export.SQL:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code>

<span class="c1">-- Nicelabel - File Repository to Disk</span>
<span class="c1">--</span>
<span class="c1">--  Written by Chris Casper</span>
<span class="c1">--  v1 - 2025.04.03</span>
<span class="c1">--</span>
<span class="c1">--  Description:</span>
<span class="c1">--  Nicelabel stores all "Document Storage" files in T-SQL. If there is an issue with version control </span>
<span class="c1">--  or a label somehow gets corrupted/changed, you would have to overwrite the entire DB to restore one </span>
<span class="c1">--  label. This script dumps all files to flat file. </span>
<span class="c1">--</span>
<span class="c1">--  Feel free to copy internally, but please keep attribution and link to https://casper.im/Nicelabel-File-Export/</span>
<span class="c1">--</span>


<span class="c1">-- Update if you changed the default Nicelabel DB name</span>
<span class="n">USE</span> <span class="n">NiceAN</span><span class="p">;</span>
<span class="k">GO</span>

<span class="c1">-- Variables</span>
<span class="k">DECLARE</span> <span class="o">@</span><span class="n">Name</span> <span class="n">NVARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span>
        <span class="o">@</span><span class="k">Data</span> <span class="nb">VARBINARY</span><span class="p">(</span><span class="k">MAX</span><span class="p">),</span>
        <span class="o">@</span><span class="n">FilePath</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">),</span>
        <span class="o">@</span><span class="n">init</span> <span class="nb">INT</span><span class="p">;</span>

<span class="k">DECLARE</span> <span class="n">cur</span> <span class="k">CURSOR</span> <span class="k">FOR</span>
<span class="k">SELECT</span> <span class="n">Name</span><span class="p">,</span> <span class="n">Content</span>
<span class="k">FROM</span> <span class="n">NiceAN</span><span class="p">.</span><span class="n">nan</span><span class="p">.</span><span class="n">DocumentStorageRepository</span><span class="p">;</span>  <span class="c1">-- This is a view, not a table</span>

<span class="k">OPEN</span> <span class="n">cur</span><span class="p">;</span>

<span class="k">FETCH</span> <span class="k">NEXT</span> <span class="k">FROM</span> <span class="n">cur</span> <span class="k">INTO</span> <span class="o">@</span><span class="n">Name</span><span class="p">,</span> <span class="o">@</span><span class="k">Data</span><span class="p">;</span>

<span class="c1">-- Loop for entire table</span>
<span class="n">WHILE</span> <span class="o">@@</span><span class="n">FETCH_STATUS</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">BEGIN</span>
    <span class="k">SET</span> <span class="o">@</span><span class="n">FilePath</span> <span class="o">=</span> <span class="s1">'C:</span><span class="se">\B</span><span class="s1">ackups</span><span class="se">\N</span><span class="s1">iceLabel</span><span class="se">\'</span><span class="s1"> + @Name;

    -- Create ADODB.Stream object
    EXEC sp_OACreate '</span><span class="n">ADODB</span><span class="p">.</span><span class="n">Stream</span><span class="s1">', @init OUTPUT;
    EXEC sp_OASetProperty @init, '</span><span class="k">Type</span><span class="s1">', 1; -- Binary mode
    EXEC sp_OAMethod @init, '</span><span class="k">Open</span><span class="s1">';
    EXEC sp_OAMethod @init, '</span><span class="k">Write</span><span class="s1">', NULL, @Data;
    EXEC sp_OAMethod @init, '</span><span class="n">SaveToFile</span><span class="s1">', NULL, @FilePath, 2; -- Overwrite if exists
    EXEC sp_OAMethod @init, '</span><span class="k">Close</span><span class="s1">';
    EXEC sp_OADestroy @init; -- Cleanup

    FETCH NEXT FROM cur INTO @Name, @Data;
END

CLOSE cur;
DEALLOCATE cur;
</span></code></pre></div></div>]]></content><author><name>Casper</name></author><category term="NiceLabel" /><category term="SQL" /><summary type="html"><![CDATA[Exporting files from Document Storage]]></summary></entry></feed>