Maybe it is best to play around with the cone twist constraint yourself. Start with this setup:
# Box B
shape = BulletBoxShape(Vec3(0.1, 0.1, 0.1))
bodyB = BulletRigidBodyNode('Box B')
bodyNP = self.worldNP.attachNewNode(bodyB)
bodyNP.node().addShape(shape)
bodyNP.setPos(0, 0, 4)
bodyNP.setHpr(0, 0, 0)
self.world.attachRigidBody(bodyB)
# Box A
shape = BulletBoxShape(Vec3(0.4, 0.2, 0.1))
bodyA = BulletRigidBodyNode('Box A')
bodyNP = self.worldNP.attachNewNode(bodyA)
bodyNP.node().addShape(shape)
bodyNP.node().setMass(1.0)
bodyNP.setPos(0, 0, 0)
bodyNP.setHpr(0, 0, 0)
self.world.attachRigidBody(bodyA)
self.world.doPhysics(0.2)
frameA = TransformState.makePosHpr(Point3(-2, 0, 0), Vec3(0, 0, 0))
frameB = TransformState.makePosHpr(Point3(0, 0, -1), Vec3(0, 0, 90))
cone = BulletConeTwistConstraint(bodyA, bodyB, frameA, frameB)
cone.setDebugDrawSize(2.0)
cone.setLimit(30, 30, 120, softness=1.0, bias=0.3, relaxation=1.0)
self.world.attachConstraint(cone)
bodyB is static, e. g. the ceiling where the lamp is hanging, and it has no rotation, just an offset of +4 up. By the way: we don’t need a shape on bodyB - it is just for cheap visualisation of bodyB’s location/orientation.
frameB is the connection point in bodyB’s reference frame. Translation is 1 down, and rotation is 90 deg on Z. The default cone opening is along the +X axis, so we need to rotate +90 around Z-axis to make it point down (we want the lamp to hang DOWN after all).
Now, we want the lamp (bodyA) offsetted from the connection point, a bit below the connection point. This is where frameA kick is. try moving it up/down. Then left/right (!!!). And try to add a rotation on frameA. If you feel comfortable try moving/rotation frameB.