Android app for HART protocol field devices (Bluetooth SPP / USB CP210x). Kotlin, MVVM, Jetpack Navigation, Material Design. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
117 lines
3.3 KiB
Python
117 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate HART Mobile app icon with smooth FSK waveform."""
|
|
|
|
import math
|
|
from PIL import Image, ImageDraw
|
|
|
|
SIZES = {
|
|
"mdpi": 48,
|
|
"hdpi": 72,
|
|
"xhdpi": 96,
|
|
"xxhdpi": 144,
|
|
"xxxhdpi": 192,
|
|
}
|
|
|
|
BG_COLOR = (25, 82, 148)
|
|
WAVE_COLOR = (255, 255, 255)
|
|
CORNER_RADIUS_RATIO = 0.22
|
|
|
|
|
|
def fsk_points(s, num=2000):
|
|
"""Generate centerline points of FSK waveform on canvas size s."""
|
|
margin_x = s * 0.10
|
|
margin_y = s * 0.28
|
|
w = s - 2 * margin_x
|
|
cy = s / 2
|
|
amplitude = (s - 2 * margin_y) * 0.45
|
|
|
|
cycles_fast = 3.0
|
|
cycles_slow = 1.5
|
|
transition = cycles_fast / (cycles_fast + cycles_slow * 2)
|
|
|
|
pts = []
|
|
for i in range(num + 1):
|
|
t = i / num
|
|
x = margin_x + t * w
|
|
if t <= transition:
|
|
phase = 2 * math.pi * cycles_fast * (t / transition)
|
|
else:
|
|
local_t = (t - transition) / (1 - transition)
|
|
phase = 2 * math.pi * cycles_fast + 2 * math.pi * cycles_slow * local_t
|
|
y = cy - amplitude * math.sin(phase)
|
|
pts.append((x, y))
|
|
return pts
|
|
|
|
|
|
def thick_curve_polygon(pts, thickness):
|
|
"""Build a filled polygon representing a thick smooth curve."""
|
|
half = thickness / 2.0
|
|
upper = []
|
|
lower = []
|
|
for i in range(len(pts)):
|
|
# Compute tangent direction
|
|
if i == 0:
|
|
dx, dy = pts[1][0] - pts[0][0], pts[1][1] - pts[0][1]
|
|
elif i == len(pts) - 1:
|
|
dx, dy = pts[-1][0] - pts[-2][0], pts[-1][1] - pts[-2][1]
|
|
else:
|
|
dx, dy = pts[i + 1][0] - pts[i - 1][0], pts[i + 1][1] - pts[i - 1][1]
|
|
length = math.sqrt(dx * dx + dy * dy)
|
|
if length < 1e-9:
|
|
nx, ny = 0, 1
|
|
else:
|
|
nx, ny = -dy / length, dx / length
|
|
x, y = pts[i]
|
|
upper.append((x + nx * half, y + ny * half))
|
|
lower.append((x - nx * half, y - ny * half))
|
|
|
|
# Polygon: upper forward + lower backward
|
|
return upper + lower[::-1]
|
|
|
|
|
|
def generate_icon(size, round_mask=False):
|
|
scale = 4
|
|
s = size * scale
|
|
img = Image.new("RGBA", (s, s), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
radius = int(s * CORNER_RADIUS_RATIO)
|
|
draw.rounded_rectangle([0, 0, s - 1, s - 1], radius=radius, fill=BG_COLOR)
|
|
|
|
pts = fsk_points(s)
|
|
thickness = s * 0.055
|
|
poly = thick_curve_polygon(pts, thickness)
|
|
draw.polygon(poly, fill=WAVE_COLOR)
|
|
|
|
# Round the endpoints with circles
|
|
for p in [pts[0], pts[-1]]:
|
|
r = thickness / 2
|
|
draw.ellipse([p[0] - r, p[1] - r, p[0] + r, p[1] + r], fill=WAVE_COLOR)
|
|
|
|
img = img.resize((size, size), Image.LANCZOS)
|
|
|
|
if round_mask:
|
|
mask = Image.new("L", (size, size), 0)
|
|
ImageDraw.Draw(mask).ellipse([0, 0, size - 1, size - 1], fill=255)
|
|
img.putalpha(mask)
|
|
|
|
return img
|
|
|
|
|
|
def main():
|
|
base = "app/src/main/res"
|
|
for density, size in SIZES.items():
|
|
icon = generate_icon(size)
|
|
icon.save(f"{base}/mipmap-{density}/ic_launcher.png")
|
|
icon_round = generate_icon(size, round_mask=True)
|
|
icon_round.save(f"{base}/mipmap-{density}/ic_launcher_round.png")
|
|
print(f" mipmap-{density}: {size}x{size}")
|
|
|
|
store = generate_icon(512)
|
|
store.save(f"{base}/mipmap-xxxhdpi/store_icon_512.png")
|
|
print(" store_icon_512.png (512x512)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|