diff --git a/pwnagotchi/defaults.toml b/pwnagotchi/defaults.toml index 532e9d6a..9523e209 100644 --- a/pwnagotchi/defaults.toml +++ b/pwnagotchi/defaults.toml @@ -67,6 +67,9 @@ main.plugins.pwndroid.display_altitude = false # show altitude on display main.plugins.pisugarx.enabled = false main.plugins.pisugarx.rotation = false main.plugins.pisugarx.default_display = "percentage" +main.plugins.pisugarx.lowpower_shutdown = true +main.plugins.pisugarx.lowpower_shutdown_level = 10 # battery percent at which the device will turn off +main.plugins.pisugarx.max_charge_voltage_protection = false #It will limit the battery voltage to about 80% to extend battery life main.plugins.session-stats.enabled = false main.plugins.session-stats.save_directory = "/var/tmp/pwnagotchi/sessions/" diff --git a/pwnagotchi/plugins/default/pisugarx.py b/pwnagotchi/plugins/default/pisugarx.py index 8d7daca6..c8c33a21 100644 --- a/pwnagotchi/plugins/default/pisugarx.py +++ b/pwnagotchi/plugins/default/pisugarx.py @@ -29,7 +29,7 @@ curve1200 = [ (3.49, 3.2), (3.1, 0.0), ] -curve1200_3= [ +curve1200_3 = [ (4.2, 100.0), # 高电量阶段 (100%) (4.0, 80.0), # 中电量阶段 (80%) (3.7, 60.0), # 中电量阶段 (60%) @@ -50,26 +50,42 @@ curve5000 = [ ] + + class PiSugarServer: def __init__(self): """ PiSugar initialization, if unable to connect to any version of PiSugar, return false """ self._bus = smbus.SMBus(1) + self.ready = False self.modle = None self.i2creg = [] self.address = 0 - self.battery_voltage = 0 - self.voltage_history = deque(maxlen=10) + self.battery_voltage = 0.00 + self.voltage_history = deque(maxlen=10) self.battery_level = 0 self.battery_charging = 0 self.temperature = 0 self.power_plugged = False self.allow_charging = True + self.lowpower_shutdown = False + self.lowpower_shutdown_level = 10 + self.max_charge_voltage_protection = False + self.max_protection_level=80 + # Start the device connection in a background thread + self.connection_thread = threading.Thread( + target=self._connect_device, daemon=True) + self.connection_thread.start() + + def _connect_device(self): + """ + Attempt to connect to the PiSugar device in a background thread. + """ while self.modle is None: if self.check_device(PiSugar_addresses["PiSugar2"]) is not None: self.address = PiSugar_addresses["PiSugar2"] - if self.check_device(PiSugar_addresses["PiSugar2"], 0Xc2) != 0: + if self.check_device(PiSugar_addresses["PiSugar2"], 0xC2) != 0: self.modle = "PiSugar2Plus" else: self.modle = "PiSugar2" @@ -77,16 +93,20 @@ class PiSugarServer: elif self.check_device(PiSugar_addresses["PiSugar3"]) is not None: self.modle = 'PiSugar3' self.address = PiSugar_addresses["PiSugar3"] + self.device_init() else: self.modle = None - logging.error( - "No PiSugar device was found. Please check if the PiSugar device is powered on.") + logging.info( + "No PiSugar device was found. Please check if the PiSugar device is powered on." + ) time.sleep(5) - - # self.update_value() + logging.info(f"{self.modle} is connected") + # Once connected, start the timer self.start_timer() while len(self.i2creg) < 256: time.sleep(1) + self.ready = True + logging.info(f"{self.modle} is ready") def start_timer(self): @@ -99,6 +119,9 @@ class PiSugarServer: """每三秒更新pisugar状态,包括触发自动关机""" while True: try: + if( self.modle == 'PiSugar2') | (self.modle == 'PiSugar2Plus'): + self.set_battery_notallow_charging() #短暂关闭充电以获取准确电池电压 + time.sleep(0.05) self.i2creg = [] for i in range(0, 256, 32): # 计算当前读取的起始寄存器地址 @@ -121,6 +144,20 @@ class PiSugarServer: ctr1 = self.i2creg[0x02] # 读取控制寄存器 1 self.power_plugged = (ctr1 & (1 << 7)) != 0 # 检查电源是否插入 self.allow_charging = (ctr1 & (1 << 6)) != 0 # 检查是否允许充电 + if self.max_charge_voltage_protection: + self._bus.write_byte_data( + self.address, 0x0B, 0x29) # 关闭写保护 + self._bus.write_byte_data(self.address, 0x20, self._bus.read_byte_data( + self.address, 0x20) | 0b10000000) + self._bus.write_byte_data( + self.address, 0x0B, 0x00) # 开启写保护 + else: + self._bus.write_byte_data( + self.address, 0x0B, 0x29) # 关闭写保护 + self._bus.write_byte_data(self.address, 0x20, self._bus.read_byte_data( + self.address, 0x20) & 0b01111111) + self._bus.write_byte_data( + self.address, 0x0B, 0x00) # 开启写保护 elif self.modle == 'PiSugar2': high = self.i2creg[0xa3] low = self.i2creg[0xa2] @@ -129,20 +166,60 @@ class PiSugarServer: 2600.0 + (((high & 0x1f) << 8) + low) * 0.26855) / 1000.0 self.power_plugged = (self.i2creg[0x55] & 0b00010000) != 0 + if self.max_charge_voltage_protection: + self.voltage_history.append(self.battery_voltage) + self.battery_level = self.convert_battery_voltage_to_level() + if (self.battery_level) > self.max_protection_level: + self.set_battery_notallow_charging() + else: + self.set_battery_allow_charging() + else: + self.set_battery_allow_charging() + elif self.modle == 'PiSugar2Plus': low = self.i2creg[0xd0] high = self.i2creg[0xd1] self.battery_voltage = ( (((high & 0b00111111) << 8) + low) * 0.26855 + 2600.0)/1000 self.power_plugged = self.i2creg[0xdd] == 0x1f - + if self.max_charge_voltage_protection: + self.voltage_history.append(self.battery_voltage) + self.battery_level = self.convert_battery_voltage_to_level() + if (self.battery_level) > self.max_protection_level: + self.set_battery_notallow_charging() + else: + self.set_battery_allow_charging() + else: + self.set_battery_allow_charging() + self.voltage_history.append(self.battery_voltage) - self.battery_level=self.convert_battery_voltage_to_level() + self.battery_level = self.convert_battery_voltage_to_level() + + if self.lowpower_shutdown: + if self.battery_level < self.lowpower_shutdown_level: + logging.info("[PiSugarX] low power shutdown now.") + self.shutdown() + pwnagotchi.shutdown() time.sleep(3) - except: - logging.error(f"read error") + except Exception as e: + logging.error(f"read error{e}") time.sleep(3) + def shutdown(self): + # logging.info("[PiSugarX] PiSugar set shutdown .") + if self.modle == 'PiSugar3': + # 10秒后关闭电源 + self._bus.write_byte_data(self.address, 0x0B, 0x29) # 关闭写保护 + self._bus.write_byte_data(self.address, 0x09, 10) + self._bus.write_byte_data(self.address, 0x02, self._bus.read_byte_data( + self.address, 0x02) & 0b11011111) + self._bus.write_byte_data(self.address, 0x0B, 0x00) # 开启写保护 + logging.info("[PiSugarX] PiSugar shutdown in 10s.") + elif self.modle == 'PiSugar2': + pass + elif self.modle == 'PiSugar2Plus': + pass + def check_device(self, address, reg=0): """Check if a device is present at the specified address""" try: @@ -278,6 +355,58 @@ class PiSugarServer: """ return self.allow_charging + def set_battery_allow_charging(self): + if self.modle == 'PiSugar3': + pass + elif self.modle == 'PiSugar2': + # 禁止 gpio2 输出 + self._bus.write_byte_data(self.address, 0x54, self._bus.read_byte_data( + self.address, 0x54) & 0b11111011) + # 开启充电 + self._bus.write_byte_data(self.address, 0x55, self._bus.read_byte_data( + self.address, 0x55) & 0b11111011) + # 开启 gpio2 输出 + self._bus.write_byte_data(self.address, 0x54, self._bus.read_byte_data( + self.address, 0x54) | 0b00000100) + elif self.modle == 'PiSugar2Plus': + # 禁止 gpio2 输出 + self._bus.write_byte_data(self.address, 0x56, self._bus.read_byte_data( + self.address, 0x56) & 0b11111011) + # 开启充电 + self._bus.write_byte_data(self.address, 0x58, self._bus.read_byte_data( + self.address, 0x58) & 0b11111011) + # 开启 gpio2 输出 + self._bus.write_byte_data(self.address, 0x56, self._bus.read_byte_data( + self.address, 0x56) | 0b00000100) + + return + + def set_battery_notallow_charging(self): + if self.modle == 'PiSugar3': + pass + elif self.modle == 'PiSugar2': + # 禁止 gpio2 输出 + print(self._bus.write_byte_data(self.address, 0x54, self._bus.read_byte_data( + self.address, 0x54) & 0b11111011)) + # 关闭充电 + self._bus.write_byte_data(self.address, 0x55, self._bus.read_byte_data( + self.address, 0x55) | 0b00000100) + # 开启 gpio2 输出 + self._bus.write_byte_data(self.address, 0x54, self._bus.read_byte_data( + self.address, 0x54) | 0b00000100) + elif self.modle == 'PiSugar2Plus': + # 禁止 gpio2 输出 + self._bus.write_byte_data(self.address, 0x56, self._bus.read_byte_data( + self.address, 0x56) & 0b11111011) + # 关闭充电 + self._bus.write_byte_data(self.address, 0x58, self._bus.read_byte_data( + self.address, 0x58) | 0b00000100) + # 开启 gpio2 输出 + self._bus.write_byte_data(self.address, 0x56, self._bus.read_byte_data( + self.address, 0x56) | 0b00000100) + + return + def get_battery_charging_range(self): """ Get the battery charging range. @@ -406,6 +535,8 @@ class PiSugarServer: """ pass + + class PiSugar(plugins.Plugin): __author__ = "jayofelony" __version__ = "1.2" @@ -418,14 +549,25 @@ class PiSugar(plugins.Plugin): def __init__(self): self._agent = None - self.is_new_model = False self.options = dict() + """ + self.options = { + 'enabled': True, + 'rotation': False, + 'default_display': 'percentage', + 'lowpower_shutdown': True, + 'lowpower_shutdown_level': 10, + 'max_charge_voltage_protection': True + } + """ self.ps = None + # logging.debug(f"[PiSugarX] {self.options}") try: self.ps = PiSugarServer() except Exception as e: # Log at debug to avoid clutter since it might be a false positive - logging.debug("[PiSugarX] Unable to establish connection: %s", repr(e)) + logging.debug( + "[PiSugarX] Unable to establish connection: %s", repr(e)) self.ready = False self.lasttemp = 69 @@ -444,7 +586,8 @@ class PiSugar(plugins.Plugin): try: return func() except Exception as e: - logging.debug("[PiSugarX] Failed to get data using %s: %s", func.__name__, e) + logging.debug( + "[PiSugarX] Failed to get data using %s: %s", func.__name__, e) return default def on_loaded(self): @@ -455,15 +598,25 @@ class PiSugar(plugins.Plugin): valid_displays = ['voltage', 'percentage', 'temp'] if self.default_display not in valid_displays: - logging.warning(f"[PiSugarX] Invalid default_display '{self.default_display}'. Using 'voltage'.") + logging.warning( + f"[PiSugarX] Invalid default_display '{self.default_display}'. Using 'voltage'.") self.default_display = 'voltage' - logging.info(f"[PiSugarX] Rotation is {'enabled' if self.rotation_enabled else 'disabled'}.") - logging.info(f"[PiSugarX] Default display (when rotation disabled): {self.default_display}") + logging.info( + f"[PiSugarX] Rotation is {'enabled' if self.rotation_enabled else 'disabled'}.") + logging.info( + f"[PiSugarX] Default display (when rotation disabled): {self.default_display}") + self.ps.lowpower_shutdown = self.options['lowpower_shutdown'] + self.ps.lowpower_shutdown_level = self.options['lowpower_shutdown_level'] + self.ps.max_charge_voltage_protection = self.options['max_charge_voltage_protection'] def on_ready(self, agent): - self.ready = True - self._agent = agent + try: + self.ready = self.ps.ready + except Exception as e: + # Log at debug to avoid clutter since it might be a false positive + logging.warning(f"[PiSugarX] {e}") + def on_internet_available(self, agent): self._agent = agent @@ -477,30 +630,52 @@ class PiSugar(plugins.Plugin): try: if request.method == "GET": if path == "/" or not path: - version = self.safe_get(self.ps.get_version, default='Unknown') + version = self.safe_get( + self.ps.get_version, default='Unknown') model = self.safe_get(self.ps.get_model, default='Unknown') - battery_level = self.safe_get(self.ps.get_battery_level, default='N/A') - battery_voltage = self.safe_get(self.ps.get_battery_voltage, default='N/A') - battery_current = self.safe_get(self.ps.get_battery_current, default='N/A') - battery_allow_charging = self.safe_get(self.ps.get_battery_allow_charging, default=False) - battery_charging_range = self.safe_get(self.ps.get_battery_charging_range, default='N/A') if self.is_new_model or model == 'Pisugar 3' else 'Not supported' - battery_full_charge_duration = getattr(self.ps, 'get_battery_full_charge_duration', lambda: 'N/A')() - safe_shutdown_level = self.safe_get(self.ps.get_battery_safe_shutdown_level, default=None) + battery_level = self.safe_get( + self.ps.get_battery_level, default='N/A') + battery_voltage = self.safe_get( + self.ps.get_battery_voltage, default='N/A') + battery_current = self.safe_get( + self.ps.get_battery_current, default='N/A') + battery_allow_charging = self.safe_get( + self.ps.get_battery_allow_charging, default=False) + battery_charging_range = self.safe_get( + self.ps.get_battery_charging_range, default='N/A') + battery_full_charge_duration = getattr( + self.ps, 'get_battery_full_charge_duration', lambda: 'N/A')() + safe_shutdown_level = self.safe_get( + self.ps.get_battery_safe_shutdown_level, default=None) battery_safe_shutdown_level = f"{safe_shutdown_level}%" if safe_shutdown_level is not None else 'Not set' - battery_safe_shutdown_delay = self.safe_get(self.ps.get_battery_safe_shutdown_delay, default='N/A') - battery_auto_power_on = self.safe_get(self.ps.get_battery_auto_power_on, default=False) - battery_soft_poweroff = self.safe_get(self.ps.get_battery_soft_poweroff, default=False) if model == 'Pisugar 3' else False - system_time = self.safe_get(self.ps.get_system_time, default='N/A') - rtc_adjust_ppm = self.safe_get(self.ps.get_rtc_adjust_ppm, default='Not supported') if model == 'Pisugar 3' else 'Not supported' - rtc_alarm_repeat = self.safe_get(self.ps.get_rtc_alarm_repeat, default='N/A') - single_tap_enabled = self.safe_get(lambda: self.ps.get_tap_enable(tap='single'), default=False) - double_tap_enabled = self.safe_get(lambda: self.ps.get_tap_enable(tap='double'), default=False) - long_tap_enabled = self.safe_get(lambda: self.ps.get_tap_enable(tap='long'), default=False) - single_tap_shell = self.safe_get(lambda: self.ps.get_tap_shell(tap='single'), default='N/A') - double_tap_shell = self.safe_get(lambda: self.ps.get_tap_shell(tap='double'), default='N/A') - long_tap_shell = self.safe_get(lambda: self.ps.get_tap_shell(tap='long'), default='N/A') - anti_mistouch = self.safe_get(self.ps.get_anti_mistouch, default=False) if model == 'Pisugar 3' else False - temperature = self.safe_get(self.ps.get_temperature, default='N/A') + battery_safe_shutdown_delay = self.safe_get( + self.ps.get_battery_safe_shutdown_delay, default='N/A') + battery_auto_power_on = self.safe_get( + self.ps.get_battery_auto_power_on, default=False) + battery_soft_poweroff = self.safe_get( + self.ps.get_battery_soft_poweroff, default=False) if model == 'Pisugar 3' else False + system_time = self.safe_get( + self.ps.get_system_time, default='N/A') + rtc_adjust_ppm = self.safe_get( + self.ps.get_rtc_adjust_ppm, default='Not supported') if model == 'Pisugar 3' else 'Not supported' + rtc_alarm_repeat = self.safe_get( + self.ps.get_rtc_alarm_repeat, default='N/A') + single_tap_enabled = self.safe_get( + lambda: self.ps.get_tap_enable(tap='single'), default=False) + double_tap_enabled = self.safe_get( + lambda: self.ps.get_tap_enable(tap='double'), default=False) + long_tap_enabled = self.safe_get( + lambda: self.ps.get_tap_enable(tap='long'), default=False) + single_tap_shell = self.safe_get( + lambda: self.ps.get_tap_shell(tap='single'), default='N/A') + double_tap_shell = self.safe_get( + lambda: self.ps.get_tap_shell(tap='double'), default='N/A') + long_tap_shell = self.safe_get( + lambda: self.ps.get_tap_shell(tap='long'), default='N/A') + anti_mistouch = self.safe_get( + self.ps.get_anti_mistouch, default=False) if model == 'Pisugar 3' else False + temperature = self.safe_get( + self.ps.get_temperature, default='N/A') ret = ''' @@ -561,7 +736,7 @@ class PiSugar(plugins.Plugin):