# main.py
from machine import Pin, I2C
import time
from i2c_lcd import I2cLcd # gunakan driver kamu (PCF8574, addr 0x27)
from hx711 import HX711 # gunakan driver hx711 yang kompatibel
# ----------------- Hardware mapping -----------------
# LCD I2C (20x4)
I2C_ID = 1
I2C_SDA = 26
I2C_SCL = 27
I2C_ADDR = 0x27
LCD_ROWS = 4
LCD_COLS = 20
# Buttons
BTN_RIGHT = Pin(7, Pin.IN, Pin.PULL_UP)
BTN_LEFT = Pin(6, Pin.IN, Pin.PULL_UP)
BTN_UP = Pin(4, Pin.IN, Pin.PULL_UP)
BTN_DOWN = Pin(5, Pin.IN, Pin.PULL_UP)
BTN_SET = Pin(2, Pin.IN, Pin.PULL_UP)
BTN_BACK = Pin(3, Pin.IN, Pin.PULL_UP)
# LEDs
LED_GREEN = Pin(1, Pin.OUT)
LED_YELLOW= Pin(0, Pin.OUT)
# Loadcell HX711
HX_DT = 17
HX_SCK = 18
# ----------------- Init peripherals -----------------
i2c = I2C(I2C_ID, sda=Pin(I2C_SDA), scl=Pin(I2C_SCL), freq=400000)
lcd = I2cLcd(i2c, I2C_ADDR, LCD_ROWS, LCD_COLS)
hx = HX711(dout=Pin(HX_DT), pd_sck=Pin(HX_SCK))
# optional default scale; recommend calibrate first
hx.set_scale(1.0)
hx.tare()
# ----------------- App state -----------------
KG_TO_LBS = 2.20462
LBS_TO_KG = 1.0 / KG_TO_LBS
# Internal canonical target stored in kg
target_kg = 11.340 # default ~25.0 lbs -> store in kg; we set a reasonable default (you can change)
current_unit = "lbs" # display default
cursor = 0 # 0=Target,1=Berat,2=Kalibrasi,3=Status
editing = False
edit_focus = 0 # 0 = value, 1 = unit (when editing Target)
blink = True
last_blink = time.ticks_ms()
BLINK_MS = 500
# SET double-press detection
pending_set = False
last_set_t = 0
SET_DOUBLE_MS = 400
# debounce
last_btn_t = 0
DEBOUNCE_MS = 150
# process state (pulling)
processing = False
status_text = "Ready"
# lcd buffer to avoid unnecessary writes
lcd_lines = [""] * LCD_ROWS
# ----------------- Helper functions -----------------
def now_ms():
return time.ticks_ms()
def pressed(pin):
"""simple debounced press detection (edge)"""
global last_btn_t
if pin.value() == 0:
t = now_ms()
if time.ticks_diff(t, last_btn_t) > DEBOUNCE_MS:
last_btn_t = t
return True
return False
def kg_to_display(kg):
return kg if current_unit == "kg" else kg * KG_TO_LBS
def display_to_kg(val_disp):
return val_disp if current_unit == "kg" else val_disp * LBS_TO_KG
def fmt_val(kg):
v = kg_to_display(kg)
# show 2 decimal for better precision
return "{:.2f}".format(v)
def write_line(row, text):
"""Overwrite a line WITHOUT clearing whole screen."""
# pad or trim to LCD_COLS
s = str(text)[:LCD_COLS].ljust(LCD_COLS)
if lcd_lines[row] != s:
lcd.move_to(0, row)
lcd.putstr(s)
lcd_lines[row] = s
def redraw_main():
"""Update only changed lines (Target, Berat, Kalibrasi, Status)."""
# Line 0: Target
if editing and cursor == 0 and edit_focus == 0 and blink:
val_str = " " * 10
else:
val_str = fmt_val(target_kg)
if editing and cursor == 0 and edit_focus == 1 and blink:
unit_str = " "
else:
unit_str = current_unit.upper()
line0 = "Target : {} {}".format(val_str.rjust(7), unit_str)
write_line(0, line0)
# Line 1: Berat realtime
try:
cur_kg = hx.get_units(3)
except Exception:
cur_kg = 0.0
line1 = "Berat : {} {}".format(fmt_val(cur_kg).rjust(7), current_unit.upper())
write_line(1, line1)
# Line 2: Kalibrasi option
prefix = ">" if cursor == 2 else " "
line2 = "{} Kalibrasi".format(prefix)
write_line(2, line2)
# Line 3: Status
prefix = ">" if cursor == 3 else " "
line3 = "{} Status : {}".format(prefix, status_text)
write_line(3, line3)
def enter_edit_mode():
global editing, edit_focus
editing = True
edit_focus = 0
def exit_edit_mode():
global editing
editing = False
def toggle_unit():
global current_unit, target_kg
if current_unit == "kg":
current_unit = "lbs"
else:
current_unit = "kg"
def start_processing():
global processing, status_text
processing = True
LED_GREEN.value(1)
status_text = "Pulling..."
def stop_processing_and_finish():
global processing, status_text
processing = False
LED_GREEN.value(0)
LED_YELLOW.value(1)
status_text = "Done"
redraw_main()
time.sleep(3)
LED_YELLOW.value(0)
status_text = "Ready"
def run_calibration():
"""Full-screen calibration steps:
1) Remove weight -> press SET (tare)
2) Put 1.00 lb -> press SET -> compute scale (internal kg)
"""
global status_text
lcd.clear()
lcd_lines[:] = [""] * LCD_ROWS
lcd.move_to(0,0)
lcd.putstr("Kalibrasi Mode")
time.sleep(1)
# Step 1
lcd.clear()
lcd_lines[:] = [""] * LCD_ROWS
lcd.move_to(0,0)
lcd.putstr("Step 1: Lepas beban")
lcd.move_to(0,1)
lcd.putstr("Tekan SET untuk OK")
# wait SET
while True:
if pressed(BTN_SET):
hx.tare()
break
if pressed(BTN_BACK):
status_text = "Calib Cancel"
return
time.sleep(0.05)
# Step 2
lcd.clear()
lcd_lines[:] = [""] * LCD_ROWS
lcd.move_to(0,0)
lcd.putstr("Step 2: Pasang 1.00 lb")
lcd.move_to(0,1)
lcd.putstr("Tekan SET untuk OK")
while True:
if pressed(BTN_SET):
# read average raw units and compute scale
raw = hx.read_average(10)
# raw corresponds to value per kg after OFFSET removed: scale = raw / mass_kg
mass_kg = 1.0 * LBS_TO_KG # 1.00 lb in kg
if raw != 0:
scale_val = raw / mass_kg
hx.set_scale(scale_val)
status_text = "Calib OK"
else:
status_text = "Calib ERR"
break
if pressed(BTN_BACK):
status_text = "Calib Cancel"
return
time.sleep(0.05)
time.sleep(1)
# restore display
lcd.clear()
lcd_lines[:] = [""] * LCD_ROWS
status_text = "Ready"
# ----------------- Initial screen -----------------
lcd.clear()
lcd_lines[:] = [""] * LCD_ROWS
lcd.move_to(0,0)
lcd.putstr("Mesin Stringer")
lcd.move_to(0,1)
lcd.putstr("Siap...")
time.sleep(1)
lcd.clear()
lcd_lines[:] = [""] * LCD_ROWS
# ----------------- Main loop -----------------
while True:
t = now_ms()
# blink toggle
if time.ticks_diff(t, last_blink) > BLINK_MS:
blink = not blink
last_blink = t
# read buttons
# UP / DOWN to move cursor when not editing, or change value when editing
if pressed(BTN_UP):
if editing:
if edit_focus == 0:
# change target: if unit is kg change by 0.5 kg; if lbs change by 1 lb
if current_unit == "kg":
target_kg += 0.5
else:
target_kg += 1.0 * LBS_TO_KG
else:
# toggle unit focus action toggles unit
toggle_unit()
else:
cursor = (cursor - 1) % 4
redraw_main()
time.sleep(0.08)
if pressed(BTN_DOWN):
if editing:
if edit_focus == 0:
if current_unit == "kg":
target_kg = max(0.0, target_kg - 0.5)
else:
target_kg = max(0.0, target_kg - 1.0 * LBS_TO_KG)
else:
toggle_unit()
else:
cursor = (cursor + 1) % 4
redraw_main()
time.sleep(0.08)
# LEFT / RIGHT used only during editing to change focus
if pressed(BTN_LEFT):
if editing:
edit_focus = (edit_focus - 1) % 2
redraw_main()
time.sleep(0.08)
if pressed(BTN_RIGHT):
if editing:
edit_focus = (edit_focus + 1) % 2
redraw_main()
time.sleep(0.08)
# SET handling: double press => edit mode toggle, single press => action
if BTN_SET.value() == 0:
# detect press event (debounced)
if pressed(BTN_SET):
now = now_ms()
if pending_set and time.ticks_diff(now, last_set_t) <= SET_DOUBLE_MS:
# double press -> toggle edit mode (only meaningful if cursor on editable field)
pending_set = False
if cursor == 0:
if editing:
exit_edit_mode()
else:
enter_edit_mode()
# if cursor==2 (kalibrasi) double-press will be treated same as single below
else:
# start pending single
pending_set = True
last_set_t = now
# small delay to avoid bouncing
time.sleep(0.05)
# finalize pending single press if timeout expired
if pending_set and time.ticks_diff(t, last_set_t) > SET_DOUBLE_MS:
pending_set = False
# treat as single press
if cursor == 0 and not editing:
# quick single press on Target -> start/stop process
if not processing:
start_processing()
else:
# if processing, stop and complete
stop_processing_and_finish()
elif cursor == 2 and not editing:
# Kalibrasi menu selected -> run calibration
run_calibration()
# otherwise ignored
# BACK button: exit edit or cancel calibration
if pressed(BTN_BACK):
if editing:
exit_edit_mode()
else:
# if in calibration loop run_calibration uses BTN_BACK too; here just reset status
status_text = "Ready"
redraw_main()
time.sleep(0.08)
# Processing: check weight vs target
if processing:
try:
cur_kg = hx.get_units(3)
except Exception:
cur_kg = 0.0
if cur_kg >= target_kg:
stop_processing_and_finish()
else:
# update status occasionally
status_text = "Pulling..."
# continue showing realtime weight via redraw_main
# Periodic screen redraw (only updates changed lines)
redraw_main()
# small sleep
time.sleep(0.05)